import { useState, useEffect } from 'react';
export function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(initialValue);
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
try {
const item = window.localStorage.getItem(key);
if (item) {
setStoredValue(JSON.parse(item));
}
} catch (error) {
console.error(`Eroare la citirea cheii "${key}":`, error);
}
}, [key]);
useEffect(() => {
const handleStorageChange = (event: StorageEvent) => {
if (event.key === key && event.newValue) {
setStoredValue(JSON.parse(event.newValue));
}
};
window.addEventListener('storage', handleStorageChange);
return () => window.removeEventListener('storage', handleStorageChange);
}, [key]);
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.error(`Eroare la salvarea cheii "${key}":`, error);
}
};
return [isMounted ? storedValue : initialValue, setValue] as const;
}Am avut anul trecut o problemă stupidă la un proiect de e-commerce cu 15k utilizatori zilnici. Aplicația era construită pe Next.js, iar băieții din echipa de frontend foloseau un hook generic de useLocalStorage copiat de pe primul rezultat de pe Google. Nimic greșit la prima vedere, doar că în Sentry aveam zilnic peste 300 de erori de Hydration failed because the initial UI does not match what was rendered on the server.
Serverul generat de Node.js nu are acces la obiectul window. Ca urmare, SSR-ul va randa mereu starea implicită (de exemplu theme = 'light'). Dacă clientul tău are deja salvat 'dark' în browser, React încearcă să facă hydrate pe un HTML diferit de ce are el în DOM. Rezultatul? Un flash deranjant, erori în consolă și, uneori, bug-uri bune de debugat la 11 noaptea.
Cum rezolvăm problema corect?
Idea e simplă: la primul render (atât pe server, cât și pe client în faza de hydration), hook-ul trebuie să returneze valoarea implicită (initialValue). Abia după ce componenta s-a montat pe client (în useEffect), citim valoarea reală din localStorage și declanșăm o re-randare.
Am adăugat și un listener pe evenimentul de storage. Astfel, dacă userul are două tab-uri deschise în același timp, starea se sincronizează automat între ele fără să fie nevoie de refresh.
Trade-off-uri reale
Soluția asta vine cu un preț: un mic re-render suplimentar pe client imediat după mount. Dacă valoarea din localStorage diferă de initialValue, utilizatorul va vedea un scurt flickering.
Nu ai cum să eviți asta 100% când folosești localStorage pe SSR, pentru că serverul pur și simplu nu are de unde să știe ce e în browserul omului. Dacă vrei să elimini complet layout shift-ul la teme vizuale sau setări critice de UI, mută starea respectivă în cookie-uri pe care le poți citi direct din request în getServerSideProps sau Server Components.
Dar pentru 90% din cazuri — filtrelor salvate, opțiuni secundare, draft-uri de formulare — hook-ul ăsta își face treaba impecabil fără să mai urle React în consolă.
Voi cum gestionați starea din localStorage pe proiecte cu Next sau Remix? Mergeți pe cookies direct sau tolerați re-renderul pe client?