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

Cum scrii un useLocalStorage care nu crapă la SSR

De Delia Petre, 25 mai 2026 · 7 vizualizări · 3 like-uri

Postat 25 mai 2026
typescript
import { useState, useEffect } from 'react';

export function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T | ((val: T) => T)) => void] {
  // Pornim mereu cu valoarea inițială pentru a evita mismatch-ul la SSR
  const [storedValue, setStoredValue] = useState<T>(initialValue);

  useEffect(() => {
    try {
      const item = window.localStorage.getItem(key);
      if (item) {
        setStoredValue(JSON.parse(item));
      }
    } catch (error) {
      console.warn(`Error reading localStorage key "${key}":`, 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(`Error setting localStorage key "${key}":`, error);
    }
  };

  return [storedValue, setValue];
}

Salutare! Dacă ai lucrat cu Next.js sau Remix, sigur ai dat peste clasica eroare: „ReferenceError: window is not defined” sau, mai rău, acel avertisment enervant de hydration mismatch în consolă. Am văzut problema asta în zeci de code-review-uri, chiar și la developeri cu experiență.

La un proiect recent, cu vreo 15k utilizatori activi, aveam un switch de temă (dark/light) salvat în localStorage. Din cauza unui hook scris pe genunchi, userii vedeau un flash alb timp de o fracțiune de secundă înainte ca tema dark să se aplice. Enervant pentru ochi, prost pentru UX.

Hai să vedem cum rezolvăm asta curat, fără să ne complicăm cu librării externe gigantice.

De ce crapă codul tău în SSR?

Problema e că Next.js randează pagina pe server înainte să o trimită în browser. În acel moment, codul tău JS rulează într-un mediu Node.js. Dacă în starea inițială a unui useState ai ceva de genul localStorage.getItem('theme'), Node se va opri instant cu eroarea că window nu este definit.

Bun, mulți rezolvă asta rapid cu un check de tipul typeof window !== 'undefined'. Dar aici apare a doua capcană: eroarea de hydration mismatch. Serverul generează un HTML bazat pe o valoare default (să zicem light), iar clientul, când citește din browser, vrea să randeze direct dark. React observă diferența între HTML-ul de pe server și cel de pe client și începe să urle în consolă.

Ca să evităm asta, trebuie să ne asigurăm de două lucruri:

  1. Nu accesăm window în timpul randării inițiale.
  2. Serverul și clientul randează exact același lucru la prima strângere de mână (first render), urmând ca valoarea reală din localStorage să fie încărcată imediat după.

Soluția: Un hook simplu și sigur

Pentru a evita mismatch-ul, inițializăm starea cu valoarea default primită ca parametru. Apoi, folosim useEffect (care rulează exclusiv pe client, după mount) ca să citim valoarea reală din stocare și să actualizăm starea.

Uită-te pe codul de mai jos. Este varianta pe care o folosesc eu de ceva timp și n-a dat niciun rateu.

Trade-off-ul sincer de care trebuie să știi

Metoda asta are un cost. Deoarece starea se actualizează doar după ce componenta s-a montat pe client, vei avea un double-render. Prima randare va fi mereu cu valoarea default (ex. light), iar a doua randare va fi cu valoarea reală (ex. dark).

Dacă ai o pagină unde elemente mari depind direct de această stare, utilizatorul ar putea sesiza un mic layout shift sau un flash vizual. Pentru teme vizuale complexe, cel mai bine e să injectezi un script inline în head-ul paginii (înainte ca React să se încarce) care să pună o clasă pe html. Dar pentru 90% din cazuri (filtre salvate, preferințe de layout, stări de sidebar), hook-ul propus este perfect și nu dă bătăi de cap.

Voi cum gestionați stările persistente în Next.js fără să stricați hydration-ul? Folosiți scripturi în head sau preferați double-render-ul?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

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