A useLocalStorage Hook keeps a piece of React state in sync with the browser's localStorage, so the value survives a page reload. It reads the stored value on first render and writes it back whenever state changes.
Use it for a theme, a saved form draft, or any preference the user expects to persist. When the value changes, the Hook writes it back so state and storage never drift.
The Hook in three parts
The Hook combines a lazy state read, a write Effect, and JSON conversion. localStorage stores only strings, so objects and numbers must be converted on the way in and out.
import { useEffect, useState } from "react";
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const stored = localStorage.getItem(key);
return stored !== null ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}Passing a function to useState runs it only during the first render, so the Hook reads localStorage once instead of on every render. getItem returns null when the key is missing, and the Hook falls back to the initial value.
The Effect runs after every committed render where value or key changed, writing the latest value back. It needs no cleanup because setItem does not create a subscription.
Use the Hook for a theme toggle
function ThemeToggle() {
const [theme, setTheme] = useLocalStorage("theme", "light");
function toggleTheme() {
setTheme(theme === "light" ? "dark" : "light");
}
return <button onClick={toggleTheme}>Theme: {theme}</button>;
}On the first visit the key is missing, so the Hook starts with light. Clicking the button stores dark, and after a reload the Hook reads dark again instead of resetting.
localStorage stores keys and values as strings. Call JSON.stringify before setItem and JSON.parse after getItem when you store objects or numbers.
Handle errors and server rendering
JSON.parse throws when the stored value is malformed, and localStorage does not exist during server rendering. The version above crashes in both cases, so add two guards.
const [value, setValue] = useState(() => {
if (typeof window === "undefined") {
return initialValue;
}
try {
const stored = localStorage.getItem(key);
return stored !== null ? JSON.parse(stored) : initialValue;
} catch {
return initialValue;
}
});The window check keeps the first render safe on the server, before returning the initial value instead of touching localStorage. The try and catch block falls back to the initial value when a stored entry is not valid JSON, so one corrupted key cannot crash the component during render.
The server check matters most in a framework that renders React on the server, such as Next.js. A component using this Hook can render there without throwing, then pick up the real stored value once the Effect runs in the browser after hydration.
What localStorage can and cannot do
- Values persist across page reloads and browser restarts.
- Data is scoped to one origin, and private mode clears it.
- It is synchronous and suits small values, not large files.
- Writes do not notify other tabs unless you listen for the storage event.
For the extraction steps, see how to create a custom Hook, and for the timing rules see when you actually need useEffect.
Rune AI
Key Insights
- Read the initial value with a lazy useState initializer.
- Write changes back through an Effect keyed on value and key.
- Store values as JSON with stringify and parse.
- Guard window access for server rendering.
- Wrap parsing in try and catch for malformed data.
Frequently Asked Questions
Why use a lazy initializer with useState?
What happens if the stored value is malformed JSON?
Does localStorage work during server rendering?
Conclusion
A useLocalStorage Hook reads its initial value lazily from localStorage and writes it back through an Effect whenever it changes. Store values as JSON and guard window access so the Hook stays safe in the browser and on the server.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Headless UI Components Explained: Logic Without Locked Styling
Understand headless UI components and how libraries like Radix give you unstyled, accessible behavior that you style yourself.