import { useState, useEffect, useCallback } from 'react';
export function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(initialValue);
// Preluăm valoarea 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.error(`Eroare la citirea cheii "${key}" din localStorage:`, error);
}
}, [key]);
const setValue = useCallback((value: T | ((val: T) => T)) => {
try {
setStoredValue((prev) => {
const valueToStore = value instanceof Function ? value(prev) : value;
window.localStorage.setItem(key, JSON.stringify(valueToStore));
// Notificăm și alte componente din același tab
window.dispatchEvent(new Event('local-storage-update'));
return valueToStore;
});
} catch (error) {
console.error(`Eroare la salvarea cheii "${key}" în localStorage:`, error);
}
}, [key]);
return [storedValue, setValue] as const;
}Am pățit-o acum vreo doi ani la un refactoring pe un proiect în Next.js cu peste 12k utilizatori activi. Am adăugat o preferință de UI în localStorage, iar la prima accesare toată pagina făcea un flash urât, urmat de clasica eroare din consolă: Text content does not match server-rendered HTML.
Problematicul hydration mismatch apare pentru că serverul Node.js nu are obiectul window și nici localStorage. Dacă inițializezi starea direct cu valoarea din storage, serverul va genera HTML folosind valoarea implicită (de exemplu false), în timp ce browserul va încerca să randeze din prima secundă valoarea salvată pe disc (să zicem true). React observă diferența la prima hidratare și aruncă eroare.
Soluția: Separarea stării de montare
Ca să scapi de eroare, trebuie să garantezi că primul render de pe client este 100% identic cu ce a generat serverul. Abia după ce componenta s-a montat pe DOM-ul clientului (adică în useEffect), citim din storage și actualizăm starea.
Iată gotcha-urile principale de care trebuie să ții cont în producție:
- Event-ul nativ
storagenu funcționează în același tab. Dacă schimbi o valoare într-o componentă, celelalte componente din același tab nu vor ști de modificare decât dacă trimiți unCustomEventmanual. - Valori corupte sau invalide. Nu presupune niciodată că ce e în
localStorageeste un JSON valid. Un utilizator își poate edita singur stocarea din DevTools sau poți schimba tu structura datelor de la un deployment la altul. Fără untry/catchlaJSON.parse, aplicația ta va da crash alb. - Layout Shift (Flash of Unstyled Content). Pentru că amâni citirea din storage până după montare, va exista un render intermediar cu valoarea de fallback. Dacă e vorba de o preferință vizuală critică (cum ar fi o temă dark/light pe tot ecranul), e posibil să ai un scurt flash.
Când NU e potrivit localStorage?
Dacă aplicația ta depinde critic de acea valoare încă din prima milisecundă (de exemplu, limba aplicației sau starea de autentificare), localStorage este alegerea greșită pentru SSR. În cazurile alea, trade-off-ul corect este să treci la cookie-uri citite în middleware sau pe server (getServerSideProps / Server Components), chiar dacă asta adaugă câțiva octeți la request-ul HTTP. localStorage e excelent pentru setări secundare, filtre de tabel sau preferințe de dashboard care nu strică vizual experiența dacă apar la 50ms după ierarhia principală.
Priviți hook-ul complet din secțiunea de cod. Conține suport pentru SSR, sincronizare între tab-uri și manipulare de erori.
Voi cum gestionați cazurile în care aveți nevoie de valori din storage la primul render fără să aveți layout shift?