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

Hook-ul useLocalStorage în Next.js fără erori de Hydration Mismatch

De Vlad Stancu, 5 aug. 2026 · 8 vizualizări · 2 like-uri

Postat 5 aug. 2026
typescript
import { useState, useEffect } from 'react';

export function useLocalStorage<T>(key: string, initialValue: T) {
  const [value, setValue] = useState<T>(initialValue);
  const [isMounted, setIsMounted] = useState(false);

  useEffect(() => {
    setIsMounted(true);
    try {
      const item = window.localStorage.getItem(key);
      if (item !== null) {
        setValue(JSON.parse(item));
      }
    } catch (error) {
      console.error(`Eroare la citirea cheii ${key}:`, error);
    }
  }, [key]);

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

  return [isMounted ? value : initialValue, setStoredValue] as const;
}

Salutare tuturor! Dacă ai lucrat vreodată cu Next.js sau Remix și ai încercat să citești ceva din localStorage direct în starea inițială dintr-un useState, 100% te-ai lovit de eroarea aia roșie infamă: "Text content does not match server-rendered HTML". Am pățit-o acum vreo doi ani la un SaaS la care lucram cu 12k utilizatori activi. Încercam să salvăm preferința de Dark Mode pe client și ne-am trezit cu un flicker oribil și erori de react-hydration în consolă.

În postarea asta vă arăt cum scriem un hook custom de React safe pentru SSR, cum funcționează de fapt și la ce gotcha-uri din producție trebuie să fiți atenți.

Problema de fond cu SSR și localStorage

React se așteaptă ca primul HTML generat pe server să fie identic, caracter cu caracter, cu ceea ce construiește el pe client la prima pasă de re-render (procesul numit hydration).

Când faci ceva de genul useState(() => localStorage.getItem('theme')), pe server (Node.js) obiectul window este undefined. Dacă încerci să previi asta cu o verificare simplă de tipul typeof window !== 'undefined', pe server valoarea va fi starea fallback (să zicem light), iar pe client va deveni instant dark. React observă această neconcordanță între HTML-ul venit din server și starea inițială din browser și aruncă un warning în consolă. În React 18, asta poate duce chiar la re-randări inutile ale întregului arbore de componente.

Soluția: Two-pass Rendering

Trucul este să forțăm hook-ul să returneze valoarea implicită pe durata primei pasaje de randare (atât pe server, cât și la prima pasă pe client), iar după ce componenta s-a montat efectiv în browser (useEffect), să citim valoarea reală din stocare.

Aruncați o privire pe codul extras din proiectele mele. Folosim un flag isMounted. Până când useEffect nu se execută în browser, hook-ul garantează că returnează initialValue, păstrând hydration-ul curat.

Trade-off-uri și Gotcha-uri din producție

Nimic nu vine gratis în software. Abordarea asta funcționează impecabil și scapă de erori, dar are niște nuanțe la care trebuie să fii atent:

  1. Layout Shift / UI Flash: Pentru că primul render afișează initialValue (de exemplu, o temă albă), utilizatorul va vedea fundalul alb o fracțiune de secundă înainte ca useEffect să ruleze și să aplice tema neagră. Dacă aplicația ta e critică pe partea vizuală, soluția pentru teme nu e un React hook, ci un mic script JS blocant pus direct în <head> care setează clasa pe tag-ul <html> înainte să se randeze body-ul.

  2. Sincronizarea între tab-uri: Varianta de bază din snippet nu ascultă de evenimentul nativ de storage. Dacă utilizatorul schimba o setare în Tab-ul A, Tab-ul B nu se va actualiza automat până la refresh. Dacă aveți nevoie de asta, adăugați un window.addEventListener('storage', ...) în interiorul useEffect-ului.

  3. Performanța la parsare: Am economisit 30% din timpul de executare pe un modul de checkout după ce am scos un obiect JSON masiv din localStorage. JSON.parse rulat sincron în useEffect blochează main thread-ul de JS. Păstrați datele din stocare cât mai mici și atomice.

Voi cum gestionați persistența stărilor pe client în proiectele de Next.js sau Remix? Folosiți cookie-uri citite direct pe server sau mergeți tot pe hook-uri de client?

Răspunsuri 0

Se încarcă răspunsurile…

Loghează-te pentru a răspunde

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