Hydration Proof

Search documentation

Find a page or section

Disallow rendering different output depending on whether the code runs on the server or in the browser.

A typeof window !== 'undefined' hydration mismatch happens because the check is false on the server and true in the browser, so a component that branches on it renders different output on each side. no-window-render-branch reports environment checks and flags such as isServer that change what a component renders.

Rulehydration-proof/no-window-render-branch
What it reportsDisallow rendering different output depending on whether the code runs on the server or in the browser
recommended / nextError
strictError
Server ComponentsSkipped with the next preset (they never hydrate)
SuggestionsNo
Optionsnone

What it reports

In render code:

  • typeof X compared with a string (typeof window !== 'undefined', 'undefined' == typeof document, typeof window.matchMedia === 'function'), where X is a browser-only global such as window, document, navigator, localStorage, sessionStorage, matchMedia, IntersectionObserver or requestAnimationFrame.
  • 'window' in globalThis and similar in checks on the global object.
  • globalThis.window / globalThis.document used as a condition.
  • Environment flags used as the condition of if, ? :, &&, || or ??: isServer, isBrowser, isClient, canUseDOM, canUseDom, isSSR, IS_BROWSER, IS_SERVER, IS_CLIENT (imports, module-level constants or globals, also as properties such as ExecutionEnvironment.canUseDOM), process.browser and import.meta.env.SSR.

Local variables and props are not flags: const isClient = useIsClient() is the recommended pattern and is not reported. A local assigned from a check (const isClient = typeof window !== 'undefined') is reported once, at the check.

Not reported here:

Browser reads behind a reported check (typeof window !== 'undefined' ? window.innerWidth : 0) are not reported again by no-browser-global-in-render.

Why the typeof window !== 'undefined' hydration mismatch happens

The check is false on the server and true during hydration, so the two renders take different branches:

server HTML:   <div class="chart-placeholder"></div>
client render: <canvas class="chart"></canvas>

React reports the mismatch and renders the whole page again on the client, which defeats server rendering.

Incorrect

function Chart({ data }) {
  if (typeof window === "undefined")
    return <div className="chart-placeholder" />;
  return <Canvas data={data} />;
}
 
function Greeting() {
  const isBrowser = typeof window !== "undefined";
  return (
    <p>{isBrowser ? `Hello from ${window.location.hostname}` : "Hello"}</p>
  );
}

Correct

function useIsClient() {
  return useSyncExternalStore(
    () => () => {},
    () => true,
    () => false
  );
}
 
function Chart({ data }) {
  const isClient = useIsClient();
  if (!isClient) return <div className="chart-placeholder" />;
  return <Canvas data={data} />;
}
 
function Greeting() {
  const [host, setHost] = useState(null);
  useEffect(() => setHost(window.location.hostname), []);
  return <p>{host ? `Hello from ${host}` : "Hello"}</p>;
}

Both render the server's output during hydration and switch after it. With Next.js, next/dynamic with ssr: false is another way to render a component only in the browser.

Options

This rule has no options. Add your own flag names with the shared setting environmentFlags:

eslint.config.mjs
{
  settings: { 'hydration-proof': { environmentFlags: ['__SERVER__', 'isNode'] } },
}

Messages

What ESLint prints for this rule, word for word:

  • <check> makes the render output depend on where the code runs. The server takes one branch and the browser the other while hydrating, so the HTML does not match. Render the same output first and switch after mount (useEffect and state), or use useSyncExternalStore with a getServerSnapshot.

When not to use it

In code that is never server-rendered.