Persisting state to localStorage while ensuring compatibility with Next.js SSR hydration is a common requirement in modern web applications.
The Hook Implementation
import { useState, useEffect } from "react";
export function useLocalStorage<T>(
key: string,
initialValue: T
): [T, (value: T | ((val: T) => T)) => void] {
// Read initial stored value safely on the client
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === "undefined") {
return initialValue;
}
try {
const item = window.localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : initialValue;
} catch (error) {
console.warn(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
});
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);
}
};
// Sync state if localStorage changes in another browser tab
useEffect(() => {
const handleStorageChange = (e: StorageEvent) => {
if (e.key === key && e.newValue) {
try {
setStoredValue(JSON.parse(e.newValue) as T);
} catch {
// Ignore parse errors
}
}
};
window.addEventListener("storage", handleStorageChange);
return () => window.removeEventListener("storage", handleStorageChange);
}, [key]);
return [storedValue, setValue];
}Example Usage
function ThemePreference() {
const [theme, setTheme] = useLocalStorage<"light" | "dark">("app-theme", "light");
return (
<button
onClick={() => setTheme((prev) => (prev === "light" ? "dark" : "light"))}
>
Current Theme: {theme}
</button>
);
}