A React Router hydration error means the first browser render differs from the
server HTML. In framework mode, React hydrates the whole document. A date in a
route, different clientLoader data, or an extension that changes <body> can
all cause a mismatch.
Common hydration errors in React Router
A React Router v7 hydration error uses React's messages. With React 19, you may see:
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.React 18 uses different wording. See React hydration error messages for the variants. Production may show only a code such as #418.
Why does a React Router hydration mismatch happen?
The default entry.client.tsx hydrates the document. React checks <html>,
<head>, and <body>, not only the route content. Look for:
| Cause | In React Router | Fix |
|---|---|---|
| Client data | A clientLoader with hydrate = true that returns other data than the loader | Below |
| The clock | new Date() or Date.now() in a route component | Time |
| Locale and timezone | toLocaleString() without an explicit locale or timeZone | Locale, timezone |
| Browser-only APIs | window, localStorage or matchMedia read during render | Browser APIs, storage |
| Invalid HTML | <div> inside <p>, <a> inside <a> | Invalid nesting |
| The document | Attributes an extension adds to <html> or <body>, tags a script adds to <head> | Extensions, scripts |
One common React Router v7 hydration mismatch comes from client data. Without
a HydrateFallback, the route component renders on the server and the
clientLoader runs during hydration. The first results from loader and
clientLoader must agree.
How to fix a React Router hydration error
Render the same thing first
For a browser-only value, start with a stable value and update it in
useEffect after hydration:
import { useEffect, useState } from "react";
export default function Greeting() {
const [name, setName] = useState<string | null>(null);
useEffect(() => {
setName(localStorage.getItem("name"));
}, []);
return <p>Hello{name ? `, ${name}` : ""}</p>;
}If the server knows the value, return it from the loader and use the same
loaderData on both sides.
Render a fallback during hydration in React Router
When a route needs browser-only data, set clientLoader.hydrate = true and
export HydrateFallback. React Router shows the fallback on the server and
during hydration. It shows the route after clientLoader finishes.
import type { Route } from "./+types/game";
import { GameBoard, loadLocalGameData } from "../game";
export async function clientLoader() {
return loadLocalGameData();
}
clientLoader.hydrate = true as const;
export function HydrateFallback() {
return <p>Loading game…</p>;
}
export default function Game({ loaderData }: Route.ComponentProps) {
return <GameBoard data={loaderData} />;
}See React Router's client data guide for other patterns.
Keep suppressHydrationWarning for small, known differences
suppressHydrationWarning hides one expected text or attribute difference.
It does not fix the page. See when it is safe.
Test every route with hydration-proof
hydration-proof tests routes in a browser and compares the server HTML with
the first client render. It selects the react-router adapter
when it finds @react-router/dev or a react-router.config.* file. The adapter
supports framework mode.
- Build and start:
react-router build(or yourbuildscript when it runsreact-router build), thenreact-router-serve ./build/server/index.js. The app is built only whenbuild/server/index.jsis missing. - Development mode:
react-router dev, with--mode developmentor--mode both. - Routes: from
react-router routes --json, inapp/orsrc/app/.:id,*and:lang?become[id],[...slug]and[[lang]], the syntaxroutes.dynamicuses. - Not-found page: a URL that does not exist is loaded too, to check that the not-found page hydrates.
- Navigation: with
--navigation, a client-side navigation throughwindow.__reactRouterDataRouter.navigateis compared with loading each route directly.
npm install -D hydration-proof
npx hydration-proof install
npx hydration-proof test --mode bothProvide example values for dynamic routes so the check can visit them:
import { defineConfig } from "hydration-proof";
export default defineConfig({
routes: {
dynamic: { "/products/[id]": ["your-stable-test-product-id"] },
},
});If you use a custom server, set server.command and server.build. Keep the
react-router adapter for route discovery. The ESLint plugin
can catch time, random values, and browser globals during render.
Related
- Remix hydration errors, React Router's predecessor
- Client-only components
- Test every route for hydration errors
- All causes of hydration errors
- Adapters