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

Cum scrii un useLocalStorage în React care nu crapă la SSR (fără hydration mismatch)

De Maria Vasilescu, 3 iul. 2026 · 15 vizualizări · 3 like-uri

Postat 3 iul. 2026
typescript
import { useState, useEffect } from 'react';

export function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T | ((val: T) => T)) => void] {
  const [storedValue, setStoredValue] = useState<T>(initialValue);
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    setMounted(true);
    try {
      const item = window.localStorage.getItem(key);
      if (item) {
        setStoredValue(JSON.parse(item));
      }
    } catch (error) {
      console.warn(`Eroare la citirea cheii "${key}" din localStorage:`, error);
    }
  }, [key]);

  const setValue = (value: T | ((val: T) => T)) => {
    try {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      if (typeof window !== 'undefined') {
        window.localStorage.setItem(key, JSON.stringify(valueToStore));
      }
    } catch (error) {
      console.warn(`Eroare la scrierea cheii "${key}" în localStorage:`, error);
    }
  };

  return [mounted ? storedValue : initialValue, setValue];
}

Să fim sinceri, cine n-a luat o palmă peste ceafă de la React când a trecut prima oară de la un SPA simplu la Next.js sau Remix? Te trezești instant în consolă cu celebrul Hydration failed because the initial UI does not match.

Am pățit-o acum un an la un proiect cu vreo 15.000 de utilizatori activi pe lună. Aveam un simplu switch de dark mode legat de un hook clasic de useLocalStorage. Pe local mergea brici. În producție, la primul refresh, site-ul clipea ciudat și consola exploda de erori roșii. Atunci m-am prins că abordarea clasică din Single Page Applications nu mai merge când ai randare pe server.

De ce apare eroarea de hydration?

Serverul nu are acces la obiectul window sau la localStorage. Când Next.js randează pagina pe server, el nu știe ce preferințe are userul în browser, așa că folosește valoarea ta implicită (să zicem, tema deschisă).

HTML-ul generat pe server ajunge în browser. În același timp, React rulează în browser, citește din localStorage că userul vrea tema închisă și încearcă să modifice DOM-ul ca să reflecte asta. React observă diferența dintre ce a trimis serverul și ce a generat el pe client și se panichează. Rezultatul? Hydration mismatch.

Compromisul: Two-pass rendering

Ca să rezolvăm problema asta curat, trebuie să acceptăm un trade-off.

Trebuie să randăm exact același lucru pe server și pe client la prima rulare. Abia după ce componenta s-a montat în DOM (adică după ce rulează useEffect), putem să citim din localStorage și să facem update la interfață.

Merge brici pentru: Evitarea erorilor de hydration și păstrarea codului simplu. E nasol pentru: Poate apărea un mic flash vizual (layout shift sau flash de culoare) dacă valoarea din localStorage diferă de cea implicită, deoarece browserul trebuie să facă un al doilea render.

Gotcha-uri de care să ții cont

Dacă folosești acest hook pentru chestii critice de layout (cum ar fi un sidebar ascuns/afișat sau teme de culori), flash-ul vizual de la primul render poate fi destul de deranjant pentru utilizatori.

Am adăugat și blocuri try/catch în cod pentru că în browserele care au cookie-urile dezactivate sau sunt în modul incognito strict, accesarea directă a localStorage poate arunca erori care îți vor bloca complet aplicația.

Dacă vrei să scapi complet de acest flash, singura soluție reală este să muți starea respectivă în cookie-uri. Cookie-urile sunt trimise la server în header-ul cererii HTTP, permițând serverului să randeze direct HTML-ul corect. Dar pentru 90% din cazuri (preferințe de filtre, date salvate temporar în formulare), hook-ul propus este mai mult decât suficient.

Voi cum gestionați starea persistentă în proiectele cu SSR? Mergeți pe varianta simplă cu useEffect sau vă complicați direct cu cookie-uri și middleware?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

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