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:
| Cause | Typical code | Fix |
|---|---|---|
| Locale and time zone | Intl.DateTimeFormat(), toLocaleString() without an explicit locale or timeZone | Locale, timezone |
| The clock | Date.now(), new Date() in a component | Time |
| Random values | random ids, Math.random() in render | Random values |
| Screen size | responsive logic that reads window or matchMedia | Media queries |
| User preferences | theme or flags read from the browser | Theme, 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:
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):
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 /postsWith --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:
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.