import { useState, useEffect } from 'react';
export function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T | ((val: T) => T)) => void] {
// Începem mereu cu valoarea default pe server pentru a evita mismatch-ul la hydration
const [storedValue, setStoredValue] = useState<T>(initialValue);
// Citim din localStorage 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(`Eroare la citirea cheii "${key}":`, error);
}
}, [key]);
// Funcția de salvare care actualizează și starea și browser-ul
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}":`, error);
}
};
return [storedValue, setValue];
}Am pățit-o acum vreo doi ani la un proiect cu 15k utilizatori activi pe zi. Am vrut să salvez preferințele de UI direct în browser și am trântit repede un hook clasic de useLocalStorage. Local totul mergea brici, dar în producție, pe Next.js, ne-am trezit cu logurile din Sentry inundate de erori urâte de tipul „Hydration failed”.
Serverul randează HTML-ul folosind valoarea ta default (să zicem, tema "light"), dar când codul ajunge în browser, React vede că în localStorage userul avea de fapt salvat "dark". Bum! Mismatch total și un ecran alb de toată frumusețea pentru o fracțiune de secundă.
De ce crapă de fapt?
Problema e simplă: pe server nu avem obiectul window. Dacă încerci să citești direct din localStorage în faza de inițializare a stării (cum ar fi useState(localStorage.getItem(...))), codul va crăpa direct la build sau la rulare pe Node.js.
Chiar dacă pui o verificare rapidă de tipul typeof window !== 'undefined', tot te lovești de hydration mismatch. React se așteaptă ca primul render din browser să fie identic, pixel cu pixel, cu ce a trimis serverul prin rețea.
Soluția: Amânarea citirii până după mount
Cea mai sigură metodă (fără să ne complicăm cu cookie-uri trimise prin headers) este să returnăm valoarea default la primul render, iar după ce componenta s-a montat în DOM (adică suntem 100% pe client, în browser), să citim valoarea reală din stocare.
Uite codul complet pe care îl folosesc acum pe toate proiectele noastre și care a redus erorile de hydration la zero.
Trade-off-ul sincer de care nu-ți zice nimeni
Metoda asta funcționează perfect și e complet sigură, dar vine cu un cost de UX. Apare acel „flicker” (flash of unstyled content). Dacă utilizatorul are tema "dark" salvată, pentru o zecime de secundă site-ul se va încărca pe "light" (valoarea de pe server), iar apoi va sări brusc pe "dark" după ce rulează useEffect-ul.
Dacă te deranjează asta la elemente critice de design, singura soluție reală este să muți starea pe server folosind cookie-uri citite la request. Dar pentru chestii mai puțin vizibile, cum ar fi dacă un sidebar e deschis sau ce filtre a aplicat userul pe o masă de date, hook-ul ăsta e sfânt.
Voi cum gestionați starea asta persistentă în Next.js? Ați trecut la cookie-uri sau trăiți cu flicker-ul din client?