# no-client-only-initial-state

> A useState initial value (window.innerWidth, navigator, a typeof window check) differs on the server and in the browser. This rule reports it in React.

Source: https://hydration.jscrate.dev/docs/rules/no-client-only-initial-state
Last updated: 2026-09-18

A useState initial value (`window.innerWidth`, `navigator.share`, a
`typeof window` check) is computed on the server and again during hydration,
so the two sides start from different state and render different HTML.
`no-client-only-initial-state` reports browser-only reads and environment
checks in the initial values of `useState`, `useReducer`, `useRef` and class
state.

| | |
| --- | --- |
| Rule | `hydration-proof/no-client-only-initial-state` |
| What it reports | Disallow initial state and refs computed from browser-only values |
| recommended / next | Warning |
| strict | Error |
| Server Components | Skipped with the next preset |
| Suggestions | No |
| Options | none |

## What it reports

Browser-only reads (`window`, `document`, `navigator`, `location`, `history`,
`screen`, `innerWidth`, `devicePixelRatio`, ... : the list of
[`no-browser-global-in-render`](https://hydration.jscrate.dev/docs/rules/no-browser-global-in-render)) in:

- the initial value of `useState` (value or lazy initializer),
- the initial argument and the init function of `useReducer`,
- the initial value of `useRef`,
- class component state: a `state = { ... }` field or `this.state = { ... }`
  in the constructor.

An initializer that reads nothing from the browser but branches on the
environment (`useState(typeof window !== 'undefined')`,
`useState(isBrowser ? 'live' : 'static')`) is reported once, at the check.

`localStorage`/`sessionStorage` and `matchMedia` in initializers are left to
[`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), including a
`typeof window` check that guards them.

## Why a useState initial value (window, navigator) breaks hydration

State initializers run during the first render, on the server and again
during hydration. A guard keeps the server from crashing, but the two sides
still start from different state:

```text
server HTML:   <nav class="menu-desktop">   (useState(() => typeof window === 'undefined' ? 1024 : window.innerWidth) → 1024)
client render: <nav class="menu-mobile">    (the same initializer → 390)
```

React keeps the server's HTML only when the first client render produces the
same output. [useEffect and two-pass rendering](https://hydration.jscrate.dev/docs/guides/useeffect-two-pass-rendering)
explains the pattern the fixes below use.

## Incorrect

```jsx
function Menu() {
  const [width] = useState(() =>
    typeof window === "undefined" ? 1024 : window.innerWidth
  );
  return width < 600 ? <MobileMenu /> : <DesktopMenu />;
}

function ShareButton() {
  const [canShare] = useState(
    typeof navigator !== "undefined" && "share" in navigator
  );
  return canShare ? <button>Share</button> : null;
}

class Page extends React.Component {
  state = { path: window.location.pathname };
  render() {
    return <p>{this.state.path}</p>;
  }
}
```

## Correct

```jsx
function Menu() {
  const [width, setWidth] = useState(1024);
  useEffect(() => {
    const update = () => setWidth(window.innerWidth);
    update();
    window.addEventListener("resize", update);
    return () => window.removeEventListener("resize", update);
  }, []);
  return width < 600 ? <MobileMenu /> : <DesktopMenu />;
}

function ShareButton() {
  const [canShare, setCanShare] = useState(false);
  useEffect(() => setCanShare("share" in navigator), []);
  return canShare ? <button>Share</button> : null;
}

class Page extends React.Component {
  state = { path: this.props.initialPath };
  componentDidMount() {
    this.setState({ path: window.location.pathname });
  }
  render() {
    return <p>{this.state.path}</p>;
  }
}
```

## Options

This rule has no options.

## Messages

What ESLint prints for this rule, word for word:

- The initial value of `<hook>` reads `<read>`, which only exists in the browser. The server renders with a fallback and the browser with the real value, so hydration does not match. Initialize with the value the server can render and update it in useEffect.
- The initial value of `<hook>` depends on `<check>`, so the server and the browser start from different state and hydration does not match. Initialize with the value the server can render and update it in useEffect.

## When not to use it

In components that are never server-rendered. The rule is a warning in
`recommended` because a component can be client-only by design (for example
behind [`next/dynamic` with `ssr: false`](https://hydration.jscrate.dev/docs/guides/next-dynamic-ssr-false)).

## Related

- [`no-browser-global-in-render`](https://hydration.jscrate.dev/docs/rules/no-browser-global-in-render)
  reports browser reads elsewhere in render.
- [`no-window-render-branch`](https://hydration.jscrate.dev/docs/rules/no-window-render-branch) reports
  environment checks elsewhere in render.
- [Browser-only APIs used during render](https://hydration.jscrate.dev/docs/causes/browser-api): the cause
  and its fixes
- [Screen size and media queries read during render](https://hydration.jscrate.dev/docs/causes/media-query),
  the usual reason to read `window.innerWidth`
