import { useState, useEffect } from 'react';
export function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T | ((val: T) => T)) => void] {
// 1. Pornim direct cu valoarea de fallback pentru a randa identic cu serverul
const [storedValue, setStoredValue] = useState<T>(initialValue);
// 2. Citim din local storage doar după ce componenta s-a montat pe client
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]);
// 3. Funcția de salvare care scrie și în state, și în storage
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];
}Am văzut zeci de proiecte Next.js care crapă în consolă cu faimosul "Hydration failed because the initial UI does not match". De cele mai multe ori, vinovatul e un custom hook de useLocalStorage scris pe genunchi. Hai să vedem cum facem unul care chiar funcționează cu SSR, fără hack-uri urâte.
De ce crapă codul clasic în SSR?
La un proiect trecut, cu vreo 15k utilizatori activi lunar, aveam o temă dark/light salvată în localStorage. Pe local totul părea perfect. Când am dat deploy în producție pe Next.js, Sentry s-a umplut instant de erori de hydration. Aveam peste 300 de log-uri de eroare pe zi.
De ce se întâmplă asta? Simplu. Serverul habar n-are ce are userul în browser. Când rulează codul pe server (la build sau la request), window este undefined. Dacă încerci să citești direct din storage în faza de inițializare a state-ului, de exemplu useState(localStorage.getItem('theme')), codul va crăpa pe server sau va genera HTML diferit de ce vrea clientul să randeze inițial. React se așteaptă ca primul render din browser să fie identic cu cel venit de pe server.
Soluția: Citirea întârziată
Ca să evităm mismatch-ul, regula de aur în SSR este: prima randare pe client trebuie să folosească aceeași valoare ca pe server. Abia după ce componenta s-a montat (adică suntem siguri că rulăm doar în browser), putem citi din localStorage și să actualizăm state-ul.
În codul de mai jos, inițializăm state-ul cu valoarea default primită ca parametru. Abia în useEffect (care rulează exclusiv pe client, după hidratare) verificăm dacă avem ceva salvat în browser și facem update-ul.
Compromisul de care trebuie să știi
Soluția asta rezolvă erorile din consolă, dar vine cu un trade-off sincer: double render.
Pentru o fracțiune de secundă, utilizatorul va vedea valoarea default (fallback-ul). Dacă el are salvată tema dark, dar default-ul tău e light, site-ul se va încărca alb, iar după o zecime de secundă va sări pe negru. Este un comportament deranjant vizual (layout shift/flash).
Cum am rezolvat asta la proiectul menționat? Pentru elemente critice (cum e tema), am renunțat la hook și am injectat un script mic, inline, direct în document care citește din storage și pune clasa pe html înainte ca React să se încarce. Pentru chestii mai puțin critice, cum ar fi filtrele salvate într-un tabel sau preferințele de sortare, hook-ul de mai jos este perfect și complet safe.
Cum gestionați voi stările astea în Next.js? Mergeți pe varianta cu script în head sau acceptați acel mic flash vizual la încărcare?