Hydration Proof

Search documentation

Find a page or section

Disallow initial state and refs computed from browser-only values.

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.

Rulehydration-proof/no-client-only-initial-state
What it reportsDisallow initial state and refs computed from browser-only values
recommended / nextWarning
strictError
Server ComponentsSkipped with the next preset (they never hydrate)
SuggestionsNo
Optionsnone

What it reports

Browser-only reads (window, document, navigator, location, history, screen, innerWidth, devicePixelRatio, ... : the list of 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 and 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:

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 explains the pattern the fixes below use.

Incorrect

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

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).