A browser API hydration mismatch happens when render code uses window,
document, navigator, or location. The server and browser then produce
different output. Start with a value both can render, and read the browser in
useEffect. If the component cannot run on the server, load it only in the
browser.
Symptoms
The message depends on whether the changed output is text, an attribute, a style, or an element:
Hydration failed because the server rendered HTML didn't match the client.
Warning: Text content did not match. Server: "Rendered for server" Client: "Rendered for Chromium"
Warning: Prop `href` did not match. Server: "/pricing?region=eu-west" Client: "/pricing?region=us-east"
Warning: Expected server HTML to contain a matching <canvas> in <div>.hydration-proof reports text as HP1001, attributes as HP1002, and styles as HP1003. React 19 may leave a mismatched attribute in place without reporting it in production, so inspect the page as well as the console.
The changed values rarely identify the browser API by themselves. hydration-proof also checks source code near the element for browser globals and server/client branches. Development builds provide better source locations, so they can give a more useful cause.
Why a browser API hydration mismatch happens
The server does not have the browser's window, document, or location.
If render code falls back to another value there, React compares that fallback
with the browser's value during hydration.
Why does typeof window cause a hydration mismatch?
Checking typeof window avoids a server crash, but it can still make the two
renders take different branches:
export function Chart({ data }: { data: number[] }) {
// false on the server, true during hydration: two different trees
if (typeof window === "undefined")
return <div className="chart-placeholder" />;
return <Canvas data={data} />;
}React compares the first browser render with the HTML actually sent by the
server. Browser-only values such as innerWidth, navigator.language, and
document.cookie can change that output whether or not you guard the read.
Do not assume typeof navigator means "in a browser" either. Some Node.js
versions have a navigator global with server-specific values.
How to fix it
Read browser values in useEffect
Start with the same value on both sides. Update it after hydration:
"use client";
import { useEffect, useState } from "react";
export function Greeting() {
// Before: typeof window !== "undefined" ? window.location.hostname : null
const [host, setHost] = useState<string | null>(null);
useEffect(() => {
setHost(window.location.hostname);
}, []);
return <p>{host ? `Hello from ${host}` : "Hello"}</p>;
}See two-pass rendering for the visual tradeoff and ways to avoid layout shifts.
Know when you are on the client with useSyncExternalStore
A hook can return false on the server and during hydration, then true
afterward. This gives components a stable first render:
import { useSyncExternalStore } from "react";
const subscribe = () => () => {};
export function useIsClient() {
return useSyncExternalStore(
subscribe,
() => true, // in the browser, after hydration
() => false // on the server and during hydration
);
}"use client";
import { useIsClient } from "./use-is-client";
export function Chart({ data }: { data: number[] }) {
const isClient = useIsClient();
if (!isClient) return <div className="chart-placeholder" />;
return <Canvas data={data} />;
}The same pattern works for a browser value that has a sensible server default, such as online status.
Render the component on the client only
If a map or chart library reads window when imported, an effect is too late:
the import itself must happen only in the browser. In Next.js, call
next/dynamic with ssr: false from a Client Component:
"use client";
import dynamic from "next/dynamic";
const Map = dynamic(() => import("./map"), {
ssr: false,
loading: () => <div className="map-placeholder" />,
});
export function MapSection() {
return <Map />;
}Both sides render the placeholder first. See next/dynamic with ssr: false and client-only components for more options.
Use the framework instead of globals
Before adding an effect, look for a server-safe source. Use your router's
pathname hook instead of location.pathname. Read request headers on the
server and pass their values down when the page needs them.
Catch it with ESLint
These rules catch different browser-only reads:
no-browser-global-in-renderreportswindow,document,navigator,location,innerWidthand other browser globals in render code.no-window-render-branchreportstypeof windowchecks and flags likeisServer,isBrowserandcanUseDOMused as conditions.no-client-only-initial-statereports browser values inuseState,useReduceranduseRefinitializers.
require-stable-server-snapshot
makes sure a useSyncExternalStore server snapshot does not read the browser.
npm install -D eslint-plugin-hydration-proofCatch it in CI
Run the browser check to find affected pages. Use both modes if you need development source locations and production behavior:
npx hydration-proof test --mode bothA probe cannot flip a typeof window branch. If a probe does
change the finding, look for a more specific cause such as
storage, media queries, or
theme.