# TanStack Start hydration errors

> A TanStack Start hydration error means the server HTML and the first client render differ. The causes, ClientOnly, selective SSR, and how to test your routes.

Source: https://hydration.jscrate.dev/docs/frameworks/tanstack-start
Last updated: 2026-09-18

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](https://tanstack.com/start/latest/docs/framework/react/guide/hydration-errors)
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:

```text
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](https://hydration.jscrate.dev/docs/causes/locale), [timezone](https://hydration.jscrate.dev/docs/causes/timezone)      |
| The clock            | `Date.now()`, `new Date()` in a component                                            | [Time](https://hydration.jscrate.dev/docs/causes/time)                                             |
| Random values        | random ids, `Math.random()` in render                                                | [Random values](https://hydration.jscrate.dev/docs/causes/random)                                  |
| Screen size          | responsive logic that reads `window` or `matchMedia`                                 | [Media queries](https://hydration.jscrate.dev/docs/causes/media-query)                             |
| User preferences     | theme or flags read from the browser                                                 | [Theme](https://hydration.jscrate.dev/docs/causes/theme), [browser APIs](https://hydration.jscrate.dev/docs/causes/browser-api) |

## 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:

```tsx title="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`):

```tsx title="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](https://hydration.jscrate.dev/docs/guides/suppresshydrationwarning).

## 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:

```bash
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:

```ts title="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](https://hydration.jscrate.dev/docs/adapters#write-an-adapter-with-defineadapter).

## Related

- [React Router hydration errors](https://hydration.jscrate.dev/docs/frameworks/react-router)
- [Vite SSR hydration](https://hydration.jscrate.dev/docs/frameworks/vite-ssr)
- [Client-only components](https://hydration.jscrate.dev/docs/guides/client-only-component)
- [Timezone hydration mismatches](https://hydration.jscrate.dev/docs/causes/timezone)
- [Adapters](https://hydration.jscrate.dev/docs/adapters)
