A Remix hydration error means the first browser render differs from the server
HTML. Remix v2 hydrates the whole document, so check route content and the
document itself. A date, different clientLoader data, or an extension-added
attribute on <body> can cause it.
Common hydration errors in Remix
Remix shows React's hydration messages. With React 18, you may see:
Hydration failed because the initial UI does not match what was rendered on the server.
Text content does not match server-rendered HTML.
There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.Production may show only a code such as #418. See React hydration error messages for the variants.
The default entry.client.tsx hydrates the document. React checks <html>,
<head>, and <body> as well as your routes. Look for:
| Cause | In Remix | 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> | Extensions |
How to fix a Remix hydration error
Render a component only in the browser
The remix-utils ClientOnly component
shows a fallback on the server and during hydration. It renders the real
component afterwards:
import { ClientOnly } from "remix-utils/client-only";
import { StoreMap, StoreMapPlaceholder } from "~/components/store-map";
export default function Stores() {
return (
<ClientOnly fallback={<StoreMapPlaceholder />}>
{() => <StoreMap />}
</ClientOnly>
);
}Use a remix-utils version compatible with your Remix app. You can also build
the same pattern with useEffect; see
client-only components.
Render a fallback while the client loader runs
When a route needs browser-only data, set clientLoader.hydrate = true and
export HydrateFallback. Remix shows the fallback on the server while the
client loader runs. Without a fallback, the server loader and browser
clientLoader must give the route the same initial data.
import { useLoaderData } from "@remix-run/react";
import { GameBoard, loadLocalGameData } from "~/game";
export async function clientLoader() {
return loadLocalGameData();
}
clientLoader.hydrate = true;
export function HydrateFallback() {
return <p>Loading game…</p>;
}
export default function Game() {
const data = useLoaderData<typeof clientLoader>();
return <GameBoard data={data} />;
}Return server values from the loader
If the server knows the value, such as a locale in a cookie, return it from the
loader. Use useLoaderData() to render that same value in the browser.
Test every route with hydration-proof
hydration-proof tests routes in a browser and compares server and client
output. It selects the remix adapter when it finds
@remix-run/dev:
| With Vite | Classic compiler | |
|---|---|---|
| Build | remix vite:build | remix build |
| Start | remix-serve ./build/server/index.js | remix-serve ./build/index.js |
| Development | remix vite:dev | remix dev |
The adapter uses the Vite commands when it finds vite.config.*. It can use
your build script when that script runs remix.
- Routes: from
remix routes --json, with:id,*and:lang?converted to[id],[...slug]and[[lang]]. - 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.__remixRouter.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 move to React Router framework mode, use its adapter and framework guide.