"use client";
import * as React from "react";
/**
* Persists state to localStorage with SSR safety and JSON serialization.
*
* @remarks
* This hook synchronizes state with localStorage, allowing data to persist
* across page refreshes and browser sessions. It is SSR-safe and returns the
* initial value on the server until hydration completes. The hook also syncs
* state across tabs/windows via the `storage` event.
*
* Error handling:
* - On initial load, a JSON parse failure falls back to `initialValue`.
* - On a cross-tab `storage` event with malformed JSON, the event is ignored
* and the current local state is preserved (to avoid wiping user state on a
* bad cross-tab message).
* - `localStorage` write failures (quota exceeded, private-mode restrictions)
* are swallowed silently — the in-memory state update still succeeds.
*
* @typeParam T - The type of the value being stored.
* @param key - The localStorage key to store the value under.
* @param initialValue - The default value to use if no value is found in localStorage.
* @returns A tuple containing the current value and a setter function.
*
* @example
* ```tsx
* function UserSettings() {
* const [theme, setTheme] = useLocalStorage("theme", "light");
*
* return (
*
* );
* }
* ```
*
* @example
* ```tsx
* function ShoppingCart() {
* const [cart, setCart] = useLocalStorage("cart", []);
*
* return (
*
* );
* }
* ```
*/
export function useLocalStorage(key: string, initialValue: T): [T, (value: T | ((prev: T) => T)) => void] {
const [storedValue, setStoredValue] = React.useState(() => {
// SSR safety: return initial value on server
if (globalThis.window === undefined) {
return initialValue;
}
try {
const item = globalThis.window.localStorage.getItem(key);
return item === null ? initialValue : (JSON.parse(item) as T);
} catch {
return initialValue;
}
});
const setValue = React.useCallback(
(value: T | ((prev: T) => T)) => {
try {
setStoredValue((currentValue) => {
const valueToStore = typeof value === "function" ? (value as (prev: T) => T)(currentValue) : value;
if (globalThis.window !== undefined) {
globalThis.window.localStorage.setItem(key, JSON.stringify(valueToStore));
}
return valueToStore;
});
} catch {
// localStorage writes can throw (quota exceeded, private mode, etc.). Fail silently.
}
},
[key],
);
React.useEffect(() => {
// SSR safety: window is not available on server
if (globalThis.window === undefined) {
return;
}
const handleStorageChange = (event: StorageEvent) => {
if (event.key !== key || event.storageArea !== globalThis.window.localStorage) {
return;
}
try {
const newValue = event.newValue === null ? initialValue : (JSON.parse(event.newValue) as T);
setStoredValue(newValue);
} catch {
// Parse failures on cross-tab storage events fall through silently.
}
};
globalThis.window.addEventListener("storage", handleStorageChange);
return () => {
globalThis.window.removeEventListener("storage", handleStorageChange);
};
}, [key, initialValue]);
return [storedValue, setValue];
}