eduardweb.
Hooks & PatternsÎncepător#nextjs#typescript#react#hooks#ssr

Cum faci un hook useLocalStorage în React fără erori de hidratare în SSR

De Ana Ionescu, 11 oct. 2026 · 5 vizualizări · 3 like-uri

Postat acum 4 zile
typescript
import { useSyncExternalStore, useCallback } from 'react';

export function useLocalStorage<T>(key: string, initialValue: T) {
  const getSnapshot = () => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? (JSON.parse(item) as T) : initialValue;
    } catch {
      return initialValue;
    }
  };

  const getServerSnapshot = () => initialValue;

  const subscribe = (callback: () => void) => {
    const onStorage = (e: StorageEvent) => {
      if (e.key === key || e.key === null) callback();
    };
    window.addEventListener('storage', onStorage);
    window.addEventListener('local-storage-change', callback);
    return () => {
      window.removeEventListener('storage', onStorage);
      window.removeEventListener('local-storage-change', callback);
    };
  };

  const value = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);

  const setValue = useCallback((val: T | ((prev: T) => T)) => {
    try {
      const current = getSnapshot();
      const next = val instanceof Function ? val(current) : val;
      window.localStorage.setItem(key, JSON.stringify(next));
      window.dispatchEvent(new Event('local-storage-change'));
    } catch (err) {
      console.warn(`Error setting localStorage key "${key}":`, err);
    }
  }, [key]);

  return [value, setValue] as const;
}

Dacă lucrezi cu Next.js sau Remix, sigur te-ai lovit măcar o dată de ecranul roșu cu Hydration failed because the server-rendered HTML didn't match the client. La un proiect de anul trecut, un panou intern cu vreo 14k useri activi, aveam tema salvată în localStorage și consola arăta ca un pom de Crăciun din cauza warning-urilor.

Problema apare mereu din același motiv: pe server nu există window sau localStorage. Serverul randează o valoare de fallback (de exemplu tema light), iar browserul găsește dark pe mașina utilizatorului. HTML-ul diferă, reconcilierea crapă.

De ce abordarea clasică e o capcană

Mulți juniori încearcă hack-ul cu typeof window !== 'undefined' ? localStorage.getItem(...) : fallback direct în argumentul de la useState. Pare logic la prima vedere, dar este greșit. Pe server componenta se randează cu fallback-ul, iar la primul render pe client se va randa direct cu valoarea din storage. React observă diferența instantaneu și aruncă eroarea.

Ca să rezolvi asta elegant, ai două căi: un state suplimentar de mounted sau useSyncExternalStore (introdus în React 18). A doua variantă e mult mai curată fiindcă a fost creată exact pentru surse externe de date din afara React-ului.

Gotcha-uri reale din producție

Înainte să arunci codul în proiect, sunt trei chestii practice de care m-am lovit:

  1. Event-ul de storage nu se declanșează în tab-ul curent. Dacă scrii în localStorage, browserul emite evenimentul storage doar către celelalte tab-uri deschise. Ca să sincronizezi componentele din aceeași pagină, trebuie să emiți manual un CustomEvent când faci update.
  2. Safari în Private Browsing sau storage plin. Dacă utilizatorul are localStorage blocat sau cota de 5MB e atinsă, setItem aruncă eroare (QuotaExceededError). Un simplu try/catch te scapă de un ecran alb neașteptat.
  3. Flicker-ul vizual (Flash of Unstyled Content). Dacă folosești hook-ul pentru temă sau date critice de layout, utilizatorul va vedea o fracțiune de secundă valoarea de pe server înainte de actualizare. Pentru teme vizuale, cel mai sănătos rămâne un script minuscul blocant de JS în <head>. Pentru orice altceva (filtre, preferințe de dashboard, token-uri non-critice), hook-ul e perfect.

Trade-off sincer

Abordarea cu useSyncExternalStore cere un pic mai mult boilerplate decât un useState naiv învelit într-un useEffect. În schimb, scapi garantat de re-randări inutile, ai cod predictibil pe SSR și nu mai vezi niciodată hydration mismatch în consolă.

Voi cum gestionați preferințele de UI persistate pe client: script blocant în HTML sau vă bazați pe hook-uri după montare?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

Doar membrii comunității pot lăsa comentarii.