# Fix a browser API hydration mismatch

> A browser API hydration mismatch happens when render code reads window or navigator, or branches on typeof window. Read browser values after mount instead.

Source: https://hydration.jscrate.dev/docs/causes/browser-api
Last updated: 2026-09-18

A browser API hydration mismatch happens when a component reads `window`,
`document`, `navigator` or `location` while it renders, or branches on
`typeof window`. The server takes one branch, the browser takes the other
during hydration, and the HTML differs. Read browser values in `useEffect`, or
render the component on the client only.

## Symptoms

The error depends on what the branch changes: text, an attribute, a style or a
whole element.

```text
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](https://hydration.jscrate.dev/docs/issues/hp1001), attributes as
[HP1002](https://hydration.jscrate.dev/docs/issues/hp1002) and inline styles as
[HP1003](https://hydration.jscrate.dev/docs/issues/hp1003), with the cause **Browser-only API used during
render**. Attribute and style differences are the dangerous ones: React 19 does
not report or fix them in production, so the page keeps the server's value.

The values alone rarely show this cause, so hydration-proof finds it in the
code: it maps the element to its source line and looks for `typeof window`,
`navigator.`, `window.` and flags like `isServer` or `canUseDOM` in the
component. That is why its confidence is lower (57–59% in the test suite) and
why development builds, with exact source lines, name it most reliably.

## Why a browser API hydration mismatch happens

The server renders in Node.js, where `window`, `document` and `location` do
not exist. Code that needs them has to do something else on the server, and
that something else is what the browser then fails to match.

### Why does typeof window cause a hydration mismatch?

The check exists to keep the server from crashing with
`ReferenceError: window is not defined`. It does that, but it also makes the
two renders take different branches:

```tsx title="chart.tsx"
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 client render with the server HTML, not with what
the server would have rendered in a browser. Any value that exists only in the
browser (`innerWidth`, `navigator.language`, `location.hostname`,
`document.cookie`, `devicePixelRatio`) has the same effect, with or without the
check.

`typeof navigator` is not a safe test either: Node.js 21 and later have a
`navigator` global, so the check is true on the server and
`navigator.userAgent` there describes Node.js.

## How to fix it

### Read browser values in useEffect

Start from a value the server can render, and switch after hydration:

```tsx title="greeting.tsx"
"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>;
}
```

[useEffect and two-pass rendering](https://hydration.jscrate.dev/docs/guides/useeffect-two-pass-rendering)
covers when the switch is visible and how to keep layout from jumping.

### Know when you are on the client with useSyncExternalStore

A hook that returns `false` during server rendering and hydration, and `true`
afterward, replaces every `typeof window` check in render:

```ts title="use-is-client.ts"
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
  );
}
```

```tsx title="chart.tsx"
"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 any browser value with a sensible server default:
`useSyncExternalStore(subscribe, () => navigator.onLine, () => true)`.

### Render the component on the client only

When a component cannot render on the server at all (a map, a chart library that
touches `window` on import), skip it during server rendering. In Next.js, load
it with `next/dynamic` and `ssr: false` from a Client Component:

```tsx title="app/map-section.tsx"
"use client";

import dynamic from "next/dynamic";

const Map = dynamic(() => import("./map"), {
  ssr: false,
  loading: () => <div className="map-placeholder" />,
});

export function MapSection() {
  return <Map />;
}
```

The server renders the placeholder, and so does the first client render. See
[next/dynamic with ssr: false](https://hydration.jscrate.dev/docs/guides/next-dynamic-ssr-false) and
[client-only components](https://hydration.jscrate.dev/docs/guides/client-only-component) for other
frameworks.

### Use the framework instead of globals

Most browser reads have a server-safe equivalent. Use `usePathname()` or your
router's hook instead of `location.pathname`, and read request headers such as
`user-agent` on the server and pass the result down, instead of reading
`navigator.userAgent` in render.

## Catch it with ESLint

Three rules cover this cause, each for a different pattern:

- [`no-browser-global-in-render`](https://hydration.jscrate.dev/docs/rules/no-browser-global-in-render)
  reports `window`, `document`, `navigator`, `location`, `innerWidth` and other
  browser globals in render code.
- [`no-window-render-branch`](https://hydration.jscrate.dev/docs/rules/no-window-render-branch) reports
  `typeof window` checks and flags like `isServer`, `isBrowser` and
  `canUseDOM` used as conditions.
- [`no-client-only-initial-state`](https://hydration.jscrate.dev/docs/rules/no-client-only-initial-state)
  reports browser values in `useState`, `useReducer` and `useRef` initializers.

[`require-stable-server-snapshot`](https://hydration.jscrate.dev/docs/rules/require-stable-server-snapshot)
makes sure a `useSyncExternalStore` server snapshot does not read the browser.

```bash
npm install -D eslint-plugin-hydration-proof
```

## Catch it in CI

`hydration-proof test` reports each difference as HP1001, HP1002, HP1003 or a
form state difference ([HP1012](https://hydration.jscrate.dev/docs/issues/hp1012)), including the attribute
and style differences React stays silent about. Run in development mode for
exact source lines, or in both modes to see what production users get:

```bash
npx hydration-proof test --mode both
```

No [probe](https://hydration.jscrate.dev/docs/probes) factor flips a `typeof window` branch, so the cause
comes from the code. When a probe changes the value, the cause is more specific:
[storage](https://hydration.jscrate.dev/docs/causes/storage), [media queries](https://hydration.jscrate.dev/docs/causes/media-query) or
[theme](https://hydration.jscrate.dev/docs/causes/theme).

## Related

- [window is not defined](https://hydration.jscrate.dev/docs/errors/window-is-not-defined)
- [Client-only components](https://hydration.jscrate.dev/docs/guides/client-only-component)
- [localStorage and sessionStorage in render](https://hydration.jscrate.dev/docs/causes/storage)
- [The no-window-render-branch rule](https://hydration.jscrate.dev/docs/rules/no-window-render-branch)
- [HP1002: attribute differs between server and client](https://hydration.jscrate.dev/docs/issues/hp1002)
