Hydration Proof

Search documentation

Find a page or section

TanStack Start hydration errors

Deterministic inputs first, client-only rendering second.

A TanStack Start hydration error means the HTML the server rendered differs from what React renders in the browser on its first pass. TanStack's own hydration errors guide names the usual causes: Intl locale and time zone formatting, Date.now(), random ids, responsive-only logic, feature flags and user preferences.

Common hydration errors in TanStack Start

TanStack Start shows React's own messages. With React 19:

Hydration failed because the server rendered HTML didn't match the client. As a result this tree will be regenerated on the client.
A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up.

A TanStack Start hydration mismatch has the same causes as in any server-rendered React app:

CauseTypical codeFix
Locale and time zoneIntl.DateTimeFormat(), toLocaleString() without an explicit locale or timeZoneLocale, timezone
The clockDate.now(), new Date() in a componentTime
Random valuesrandom ids, Math.random() in renderRandom values
Screen sizeresponsive logic that reads window or matchMediaMedia queries
User preferencestheme or flags read from the browserTheme, browser APIs

How to fix a TanStack Start hydration error

TanStack's guide lists five strategies.

Make the server and the client agree

Pick the locale and time zone on the server, deterministically, and use the same values on the client. The guide recommends a cookie as the source of truth, with the Accept-Language header as a fallback, computed once on the server and passed down. Until the browser has told the server its time zone, render in UTC.

Let the client report its environment

On the first visit, set a cookie with the browser's time zone from an effect. The next request renders on the server with the right zone, and the first render never depends on the browser.

Render unstable UI on the client only

ClientOnly from @tanstack/react-router skips server rendering for its children and shows the fallback until the page has hydrated:

src/components/last-seen.tsx
import { ClientOnly } from "@tanstack/react-router";
import { RelativeTime } from "./relative-time";
 
export function LastSeen({ ts }: { ts: number }) {
  return (
    <ClientOnly fallback={<span>—</span>}>
      <RelativeTime ts={ts} />
    </ClientOnly>
  );
}

Turn off server rendering for a route

Selective SSR renders a route's data on the server but not its component (ssr: "data-only"), or skips the server for that route (ssr: false):

src/routes/unstable.tsx
import { createFileRoute } from "@tanstack/react-router";
import { ExpensiveViz } from "../components/expensive-viz";
 
export const Route = createFileRoute("/unstable")({
  ssr: "data-only",
  component: ExpensiveViz,
});

Suppress the warning as a last resort

suppressHydrationWarning on a small, known-different element keeps the server's text without a warning. Use it sparingly; see when suppressHydrationWarning is safe.

Test every route with hydration-proof

hydration-proof has no built-in TanStack Start adapter, so it does not discover routes or know the build commands on its own. It still tests any server-rendered React page, in one of three ways.

Test an app you started

Start the production build yourself, then point hydration-proof at it and name the routes:

npx hydration-proof test --url http://localhost:3000 --route / --route /posts

With --url, nothing is built or started, and only the routes you name are tested. Add --sitemap to test the pages in your sitemap, or --crawl to follow links from the tested pages.

Let hydration-proof build and start it

Use adapter: "none" and give it your commands:

hydration-proof.config.ts
import { defineConfig } from "hydration-proof";
 
export default defineConfig({
  adapter: "none",
  server: {
    build: "npm run build",
    command: "npm run start", // must listen on process.env.PORT
  },
  routes: { paths: ["/", "/posts", "/posts/1"] },
});

hydration-proof picks a free port and passes it in the PORT environment variable; {port} in the command is replaced with it too. Without an adapter, it cannot tell whether a build already exists, so it runs server.build on every run. Set server.buildWhen: "never" when an earlier CI step builds the app.

npx hydration-proof doctor shows which adapter adapter: "auto" would pick. An app with react-dom and a start script matches the generic node adapter, which runs your build, start and dev scripts; routes still come from the config.

Write an adapter

To discover routes from src/routes and skip rebuilding when a build exists, write a small adapter with defineAdapter: a detect function, the commands, a buildOutput file and a discoverRoutes function. See writing an adapter.