Building a useLocalStorage Hook
In this page:
Why Sync State with localStorage
Normal React state resets every time the page reloads, since it only lives in memory. localStorage persists data in the browser even after the tab is closed. A useLocalStorage hook combines the two, so a value behaves like normal state but also survives a refresh.
Note: Good use cases are theme preference, a saved draft, or 'remember me' style settings — anything you'd want to survive a reload.
Warning: Don't store sensitive data (like tokens or passwords) in localStorage — it's readable by any JavaScript running on the page, including malicious scripts.
Example: Why Sync State with localStorage
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function useLocalStorage(key, initialValue) {
const [value, setValue] = React.useState(() => {
const saved = localStorage.getItem(key);
return saved !== null ? JSON.parse(saved) : initialValue;
});
React.useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
function App() {
const [name, setName] = useLocalStorage("name", "Guest");
return <input value={name} onChange={e => setName(e.target.value)} />;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>
Reading the Saved Value on First Render
useState accepts a function as its initial value (called a lazy initializer), which only runs once on the first render. This is the right place to check localStorage for a previously saved value, since checking it directly in the function body would run on every render.
Note: Always pass a function to useState (() => ...) rather than calling localStorage.getItem() directly as the argument, to avoid re-reading storage on every render.
Warning: If the stored JSON is malformed (e.g. edited manually in devtools), JSON.parse will throw — wrap it in a try/catch to fall back to the initial value.
Example: Reading the Saved Value on First Render
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function useLocalStorage(key, initialValue) {
const [value] = React.useState(() => {
try {
const saved = localStorage.getItem(key);
return saved !== null ? JSON.parse(saved) : initialValue;
} catch { return initialValue; }
});
return value;
}
function App() {
const count = useLocalStorage("count", 0);
return <p>Loaded count: {count}</p>;
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>
Writing Back on Every Change
A useEffect that watches the state value writes it back to localStorage every time it changes, using JSON.stringify since localStorage can only store strings. This keeps the saved copy always in sync with the current React state.
Note: Include both the key and the value in the dependency array so the hook stays correct even if the key itself is dynamic.
Warning: Writing to localStorage on every keystroke of a fast-changing input can be wasteful — consider debouncing the write for large or frequently-changing values.
Example: Writing Back on Every Change
<!DOCTYPE html>
<html>
<head>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
function useLocalStorage(key, initialValue) {
const [value, setValue] = React.useState(initialValue);
React.useEffect(() => { localStorage.setItem(key, JSON.stringify(value)); }, [key, value]);
return [value, setValue];
}
function App() {
const [notes, setNotes] = useLocalStorage("notes", "");
return (
<div>
<textarea value={notes} onChange={e => setNotes(e.target.value)} />
<p>Saved: {notes.length} characters</p>
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />);
</script>
</body>
</html>
- Forgetting that localStorage only stores strings, so objects/arrays must be JSON.stringify'd before saving and JSON.parse'd when reading back.
- Not wrapping localStorage access in a try/catch, which can throw in private browsing mode or when storage is full.
- Using the same localStorage key for two different pieces of state, causing them to silently overwrite each other.
- useLocalStorage is a custom hook that syncs a piece of React state with the browser's localStorage.
- It reads the saved value on initial render and writes back to localStorage whenever the state changes.
- JSON.stringify/JSON.parse are needed because localStorage only stores strings.
- This pattern is commonly used for theme preferences, form drafts, and small settings that should survive a page refresh.
Available since React 16.8 (Hooks introduction); localStorage itself is supported in all modern browsers (IE8+).
Chapter Quiz — Complete all 10 topics to unlock
0/10 topics done
Complete these topics first: