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

Cum scrii un useLocalStorage care nu-ți crapă aplicația Next.js la hydration

De Radu Grigore, 28 mai 2026 · 7 vizualizări · 2 like-uri

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

export function useLocalStorage<T>(key: string, initialValue: T) {
  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);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.warn(`Error setting localStorage key "${key}":`, error);
    }
  };

  return [storedValue, setValue] as const;
}

Dacă folosești Next.js sau Remix, probabil ai văzut deja eroarea aia roșie de hydration mismatch când ai încercat să citești direct din localStorage. Am pățit asta pe un proiect de e-commerce cu peste 15.000 de utilizatori activi, unde am vrut să salvez coșul de cumpărături direct în browser. Soluția e simplă, dar trebuie să fii foarte atent la ciclul de viață al componentelor.

De ce apare eroarea de hydration?

Serverul de React (SSR) rulează codul tău în Node.js, unde obiectul window nu există. Dacă încerci să citești din localStorage direct în faza de inițializare a stării (useState(localStorage.getItem('key'))), serverul va genera un HTML folosind o valoare implicită. Asta în cel mai bun caz, fiindcă în cel mai rău caz aplicația va crăpa direct pe server pentru că ai apelat o metodă care nu există în Node.

Când HTML-ul generat ajunge în browser, React încearcă să facă „hidratarea” (hydration). Browserul rulează exact același cod, vede că în localStorage există deja o valoare salvată anterior și generează un arbore DOM diferit de cel venit de pe server. React observă imediat diferența, se panichează și aruncă eroarea în consolă. În unele cazuri, asta poate duce la bug-uri ciudate de UI sau chiar la re-randări infinite.

Soluția: Amânarea citirii până după mount

Trucul este să inițializezi starea cu o valoare implicită neutră, care este garantat aceeași și pe server, și pe client la prima rulare. Abia după ce componenta s-a montat fizic în browser (moment marcat de rularea hook-ului useEffect), citim valoarea reală din localStorage și actualizăm starea React.

Uită-te pe codul din tab-ul de mai jos. Este o implementare curată, tipizată în TypeScript, care nu dă erori de SSR.

De asemenea, am inclus blocuri try-catch. Este un detaliu extrem de important pe care mulți juniori îl omit. Dacă utilizatorul tău folosește Safari în modul Incognito sau are cookie-urile complet blocate din setările de securitate, orice acces direct la window.localStorage va arunca o eroare fatală și va bloca execuția întregii aplicații. Prin prinderea erorii, ne asigurăm că aplicația continuă să funcționeze, chiar dacă starea nu se va salva la refresh.

Compromisul: Efectul de flash (Layout Shift)

Nimic nu este gratis în web development. Folosind această abordare, ai rezolvat eroarea de hydration, dar ai introdus un alt compromis: un mic „flicker” vizual.

Dacă folosești acest hook pentru a salva tema vizuală (dark sau light mode) și utilizatorul are setată tema dark, pagina se va randa inițial ca light (valoarea implicită trimisă de server). După aproximativ 50 de milisecunde, useEffect-ul se va executa în browser, va citi valoarea corectă din browser și va schimba tema în dark. Utilizatorul va vedea un flash alb destul de deranjant pe ecrane mari.

Pentru stări non-vizuale (cum ar fi un draft de text într-un formular sau un istoric de căutări recente), acest delay este complet insesizabil, iar hook-ul funcționează perfect. Am economisit mult timp și am redus erorile din Sentry la zero pe proiectul meu folosind exact această metodă pentru formularele mari de checkout.

Dacă ai nevoie de el pentru teme sau elemente de branding sensibile la layout shift, mai bine folosești cookie-uri trimise prin head-ul HTTP sau injectezi un mic script inline în <head>-ul paginii, care rulează blocant înainte ca React să se încarce.

Voi cum gestionați starea persistentă în aplicațiile cu SSR? Preferați să folosiți cookie-uri pentru a evita complet flash-ul vizual sau mergeți pe varianta simplă cu client-side hydration?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

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