useLocalStorage

A theme choice, a collapsed sidebar or a draft message should survive a reload. This Hook keeps useState's API — a value and a setter that accepts an updater function — and mirrors every change into localStorage.

State that survives a reloadJavaScript
import { useState, useCallback } from 'react';
export function useLocalStorage(key, initial) {
  const [value, setValue] = useState(() => {
    try {
      const raw = localStorage.getItem(key);
      return raw === null ? initial : JSON.parse(raw);
    } catch { return initial; }          // private mode, disabled storage, bad JSON
  });
  const set = useCallback(next => setValue(prev => {
    const resolved = next instanceof Function ? next(prev) : next;
    try { localStorage.setItem(key, JSON.stringify(resolved)); } catch {}
    return resolved;
  }), [key]);
  return [value, set];
}

Call it exactly like useState: const [theme, setTheme] = useLocalStorage('theme', 'light');. The write happens inside the setter, not in an Effect, which avoids a pointless write on every mount.

Three details are easy to get wrong. localStorage stores strings only, so anything else needs the JSON round trip — and a value written by an older version of your app may no longer parse, which is what the first catch is for. Access can throw outright: Safari 10 's private mode and a full quota both reject writes, and localStorage does not exist during server rendering, so a Next.js 10,514 page must read it in an Effect or guard on typeof window. Finally, the Hook does not notice changes made in another tab; if that matters, listen for the storage event or wire the key through useSyncExternalStore (useSyncExternalStore).