Hydration Proof

Search documentation

Find a page or section

Disallow reading localStorage or sessionStorage while a component renders, including state initializers.

Reading localStorage in useState, or anywhere else in render, gives the two renders different values: the server has no storage and renders the default, while the browser renders the stored value during hydration. no-storage-in-initial-render reports every localStorage and sessionStorage read in render code, including state initializers.

Rulehydration-proof/no-storage-in-initial-render
What it reportsDisallow reading localStorage or sessionStorage while a component renders, including state initializers
recommended / nextError
strictError
Server ComponentsSkipped with the next preset (they never hydrate)
SuggestionsNo
Optionsnone

What it reports

Any read of localStorage or sessionStorage (bare, or through window., self., globalThis.) in render code, including the initial values of useState, useReducer, useRef and class component state. Reads behind a typeof window !== 'undefined' check are reported too: the check keeps the server from crashing, but the two renders still differ. typeof localStorage on its own is an environment check and belongs to no-window-render-branch.

Why localStorage in useState breaks hydration

The server has no storage, so it renders the default. The browser renders the stored value during hydration:

server HTML:   <html class="light">   (no stored theme)
client render: <html class="dark">    (localStorage.theme === 'dark')

React 19 keeps the server's class="light" without a warning in production, so the page stays in the wrong theme until something re-renders it. A text difference makes React discard the server HTML and render again.

Incorrect

function ThemeToggle() {
  const [theme, setTheme] = useState(
    () => localStorage.getItem("theme") ?? "light"
  );
  return (
    <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
      {theme}
    </button>
  );
}
 
function Draft() {
  const saved =
    typeof window !== "undefined" ? sessionStorage.getItem("draft") : "";
  return <textarea defaultValue={saved} />;
}

Correct

// Start from the default, then load the stored value.
function ThemeToggle() {
  const [theme, setTheme] = useState("light");
  useEffect(() => {
    setTheme(localStorage.getItem("theme") ?? "light");
  }, []);
  return (
    <button onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
      {theme}
    </button>
  );
}
 
// Or subscribe to storage with a server snapshot.
function useStoredTheme() {
  return useSyncExternalStore(
    subscribeToStorage,
    () => localStorage.getItem("theme") ?? "light",
    () => "light"
  );
}

For a theme that must be right before the first paint, store it in a cookie and read it on the server, or set the class with an inline script before React loads and put suppressHydrationWarning on <html>. Theme hydration mismatches walks through both.

Options

This rule has no options.

Messages

What ESLint prints for this rule, word for word:

  • <read> is read during render. Storage only exists in the browser, so the server renders without the stored value and hydration sees a different result. Render the default first and read storage in useEffect, or use useSyncExternalStore with a getServerSnapshot.
  • The initial value of <hook> reads <read>. Storage only exists in the browser, so the server renders the default while the browser starts from the stored value and hydration does not match. Start from the default and load the stored value in useEffect.

When not to use it

In components that are never server-rendered.