← ./articles

Next.js localStorage Hydration: initializeWithValue false Is Not the Whole Fix

In Next.js, localStorage is a client-side API. The server cannot read it.

That means any state initialized from localStorage can create a hydration mismatch if the server-rendered HTML and the first client render disagree.

usehooks-ts helps with this:

const [tabs, setTabs] = useLocalStorage("tabs", [], {
  initializeWithValue: false,
});

But initializeWithValue: false is not the whole fix. It prevents the initial mismatch, but your effects can still run against the pre-hydration default value.

The subtle bug

This looks reasonable:

const [tabs, setTabs] = useLocalStorage<Tab[]>("tabs", [], {
  initializeWithValue: false,
});

useEffect(() => {
  if (tabs.length === 0) {
    setTabs([createDefaultTab()]);
  }
}, []);

The effect runs once after the first render. At that moment, tabs is still the default [], not the value from localStorage.

If stored tabs exist, this effect can still create a duplicate default tab before hydration finishes.

Add a hydration guard

Use a ref to skip the first effect pass, and include the stored value in the dependency array:

const [tabs, setTabs] = useLocalStorage<Tab[]>("tabs", [], {
  initializeWithValue: false,
});

const hydrated = useRef(false);

useEffect(() => {
  if (!hydrated.current) {
    hydrated.current = true;
    return;
  }

  if (tabs.length === 0) {
    setTabs([createDefaultTab()]);
  }
}, [tabs, setTabs]);

Now the effect can react after the hook reads the real stored value.

Use typed empty defaults

Avoid undefined defaults for arrays and objects that drive logic:

// Avoid
useLocalStorage<Tab[] | undefined>("tabs", undefined, {
  initializeWithValue: false,
});

// Prefer
useLocalStorage<Tab[]>("tabs", [], {
  initializeWithValue: false,
});

Typed empty defaults make render logic simpler. Combine them with the hydration guard when effects create derived state.

When not to use initializeWithValue false

Do not use initializeWithValue: false for identifiers that must be available immediately to reconnect to server resources.

Examples:

  • WebSocket session IDs
  • PTY session IDs
  • room IDs used on mount

Those values may need synchronous initialization, because using a default ID for the first render can connect to the wrong server-side resource.

Summary

For UI preferences and simple persisted client state, initializeWithValue: false is useful.

But if an effect reads that value and creates side effects, add a hydration guard and include the stored value in the dependency array. Otherwise, your code can act on the pre-hydration default and create duplicate or stale state.

References