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.