# no-window-render-branch

> The typeof window !== 'undefined' hydration mismatch: the check is false on the server and true in the browser. This rule reports render branches on it.

Source: https://hydration.jscrate.dev/docs/rules/no-window-render-branch
Last updated: 2026-09-18

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.

| | |
| --- | --- |
| Rule | `hydration-proof/no-window-render-branch` |
| What it reports | Disallow rendering different output depending on whether the code runs on the server or in the browser |
| recommended / next | Error |
| strict | Error |
| Server Components | Skipped with the next preset |
| Suggestions | No |
| Options | none |

## What it reports

In [render code](https://hydration.jscrate.dev/docs/eslint#what-counts-as-render):

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

- checks in effects, event handlers and other code that runs after hydration;
- checks in the initial value of `useState`, `useReducer`, `useRef` or class
  state: [`no-client-only-initial-state`](https://hydration.jscrate.dev/docs/rules/no-client-only-initial-state)
  reports those;
- checks that guard a `localStorage`/`sessionStorage` read or a `matchMedia`
  call: [`no-storage-in-initial-render`](https://hydration.jscrate.dev/docs/rules/no-storage-in-initial-render)
  and [`no-match-media-in-render`](https://hydration.jscrate.dev/docs/rules/no-match-media-in-render) report
  the read.

Browser reads behind a reported check
(`typeof window !== 'undefined' ? window.innerWidth : 0`) are not reported
again by [`no-browser-global-in-render`](https://hydration.jscrate.dev/docs/rules/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:

```text
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

```jsx
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

```jsx
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`](https://hydration.jscrate.dev/docs/guides/next-dynamic-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](https://hydration.jscrate.dev/docs/eslint#shared-settings) `environmentFlags`:

```js title="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.

## Related

- [`no-browser-global-in-render`](https://hydration.jscrate.dev/docs/rules/no-browser-global-in-render):
  browser reads outside a check
- [`require-stable-server-snapshot`](https://hydration.jscrate.dev/docs/rules/require-stable-server-snapshot)
  reports checks inside `getServerSnapshot`.
- [Browser-only APIs used during render](https://hydration.jscrate.dev/docs/causes/browser-api): the cause
  and its fixes
- [Client-only components](https://hydration.jscrate.dev/docs/guides/client-only-component): the patterns
  that replace the check
- [Hydration failed because the server rendered HTML didn't match the client](https://hydration.jscrate.dev/docs/errors/hydration-failed-server-rendered-html-didnt-match-client)
