# no-storage-in-initial-render

> Reading localStorage in useState or anywhere in render makes the server render the default and the browser the stored value. This rule reports it.

Source: https://hydration.jscrate.dev/docs/rules/no-storage-in-initial-render
Last updated: 2026-09-18

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.

| | |
| --- | --- |
| Rule | `hydration-proof/no-storage-in-initial-render` |
| What it reports | Disallow reading localStorage or sessionStorage while a component renders, including state initializers |
| recommended / next | Error |
| strict | Error |
| Server Components | Skipped with the next preset |
| Suggestions | No |
| Options | none |

## What it reports

Any read of `localStorage` or `sessionStorage` (bare, or through `window.`,
`self.`, `globalThis.`) in [render code](https://hydration.jscrate.dev/docs/eslint#what-counts-as-render),
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`](https://hydration.jscrate.dev/docs/rules/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:

```text
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

```jsx
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

```jsx
// 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](https://hydration.jscrate.dev/docs/causes/theme) 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.

## Related

- [`no-client-only-initial-state`](https://hydration.jscrate.dev/docs/rules/no-client-only-initial-state)
  covers other browser values in initial state.
- [`require-stable-server-snapshot`](https://hydration.jscrate.dev/docs/rules/require-stable-server-snapshot)
  reports storage reads inside `getServerSnapshot`.
- [localStorage and sessionStorage read during render](https://hydration.jscrate.dev/docs/causes/storage):
  the cause and its fixes
- [Zustand persist hydration errors](https://hydration.jscrate.dev/docs/guides/zustand-hydration-error),
  the same problem inside a store
