React useLocalStorage Hook
Persisting a piece of state to localStorage so it survives a page reload is one of the most-requested custom hooks. This snippet covers the basic JSON-serialised hook, an SSR-safe variant that lazily reads only on the client, and a cross-tab sync version that listens for the storage event. Pick the one that matches whether your app is client-only, server-rendered, or multi-window.
496 views
4
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
try {
const raw = localStorage.getItem(key);
return raw !== null ? JSON.parse(raw) : initialValue;
} catch {
return initialValue;
}
});
const update = (next) => {
const resolved = typeof next === 'function' ? next(value) : next;
setValue(resolved);
try { localStorage.setItem(key, JSON.stringify(resolved)); } catch {}
};
return [value, update];
}
// Smoke-test: a tiny mock that respects lazy initializers.
function useState(init) {
const v = typeof init === 'function' ? init() : init;
return [v, () => {}];
}
const [theme, setTheme] = useLocalStorage('theme', 'light');
console.log('theme:', theme);The lazy initializer (() => ...) reads from localStorage only once, which avoids parsing JSON on every render. The setter accepts either a value or a functional updater, mirroring the setState API so callers can keep using setTheme((t) => t === 'light' ? 'dark' : 'light'). Wrapping reads and writes in try/catch handles two real failures: quota exceeded on the write side, and corrupt JSON or disabled storage on the read side. This is the right shape for a single-tab, client-only app.
function useLocalStorageSSR(key, initialValue) {
const [value, setValue] = useState(initialValue);
useEffect(() => {
if (typeof window === 'undefined') return;
try {
const raw = window.localStorage.getItem(key);
if (raw !== null) setValue(JSON.parse(raw));
} catch {}
}, [key]);
const update = (next) => {
const resolved = typeof next === 'function' ? next(value) : next;
setValue(resolved);
if (typeof window !== 'undefined') {
try { window.localStorage.setItem(key, JSON.stringify(resolved)); } catch {}
}
};
return [value, update];
}
function useEffect(fn) { fn(); }
const [token] = useLocalStorageSSR('token', null);
console.log('token on first render:', token);Server rendering breaks the basic version because localStorage is undefined on the server, and reading it during the initial render also breaks React's hydration check (the server and client must produce identical markup on first render). The SSR-safe variant returns initialValue synchronously and reads from localStorage inside useEffect, which only runs on the client. The typeof window guard makes the hook safe in any framework that pre-renders. The trade-off is one extra render once the persisted value lands.
function useLocalStorageSync(key, initialValue) {
const [value, setValue] = useState(initialValue);
useEffect(() => {
if (typeof window === 'undefined') return undefined;
try {
const raw = window.localStorage.getItem(key);
if (raw !== null) setValue(JSON.parse(raw));
} catch {}
const onStorage = (e) => {
if (e.key !== key) return;
try { setValue(e.newValue ? JSON.parse(e.newValue) : initialValue); } catch {}
};
window.addEventListener('storage', onStorage);
return () => window.removeEventListener('storage', onStorage);
}, [key]);
const update = (next) => {
const r = typeof next === 'function' ? next(value) : next;
setValue(r);
if (typeof window !== 'undefined') {
try { window.localStorage.setItem(key, JSON.stringify(r)); } catch {}
}
};
return [value, update];
}
const [seat] = useLocalStorageSync('seat', 'A');
console.log('seat:', seat);The browser fires a storage event on every other tab when one tab writes to localStorage (the writing tab itself does not receive it). Subscribing to that event lets you broadcast logout, theme changes, or shopping-cart updates across windows without any extra infrastructure. The cleanup that removes the listener is essential, otherwise stale handlers from unmounted components keep firing. Combine this with the SSR-safe pattern from the previous accordion when both constraints apply.
