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 (they never hydrate) |
| 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) 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 orthis.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).
Related
no-browser-global-in-renderreports browser reads elsewhere in render.no-window-render-branchreports environment checks elsewhere in render.- Browser-only APIs used during render: the cause and its fixes
- Screen size and media queries read during render,
the usual reason to read
window.innerWidth