useLocalStorage
A robust React hook for persisting state in window.localStorage with SSR safety and cross-tab synchronization.Copy Markdown
Overview
useLocalStorage acts like useState, but it automatically syncs the value with the browser's localStorage. This means data persists even after the page is refreshed or the browser is closed.
Key Features:
- SSR Safe: Prevents errors during server-side rendering (Next.js/Vite environment).
- Cross-Tab Syncing: Automatically updates state when changes occur in other browser tabs/windows.
- Type Safe: Uses TypeScript generics to ensure your data remains correctly typed.
- Functional Updates: Supports passing an updater function
(prev => newVal).
Installation
Automatic (CLI)
npx shadcn@latest add @nurav-ui/use-local-storageAlternatively, install via direct URL:
npx shadcn@latest add https://nurav-ui.vercel.app/r/use-local-storage.jsonManual Setup
Create the hook file
Copy the source into hooks/use-local-storage.ts:
import { useState, useCallback, useEffect } from "react";
export function useLocalStorage<T>(key: string, initialValue: T): [T, (value: T | ((val: T) => T)) => void] {
const readValue = useCallback((): 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;
}
}, [key, initialValue]);
const [storedValue, setStoredValue] = useState<T>(readValue);
const setValue = useCallback((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));
window.dispatchEvent(new Event("local-storage"));
}
} catch (error) {
console.warn(`Error setting localStorage key “${key}”:`, error);
}
}, [key, storedValue]);
useEffect(() => {
const handleStorageChange = (event: StorageEvent) => {
if (event.key === key) setStoredValue(readValue());
};
const handleCustomEvent = () => setStoredValue(readValue());
window.addEventListener("storage", handleStorageChange);
window.addEventListener("local-storage" as any, handleCustomEvent);
return () => {
window.removeEventListener("storage", handleStorageChange);
window.removeEventListener("local-storage" as any, handleCustomEvent);
};
}, [key, readValue]);
return [storedValue, setValue];
}Usage
Theme Toggler Example
Persisting user theme preference across sessions.
import { useLocalStorage } from '@/hooks/use-local-storage';
export default function ThemeToggle() {
const [theme, setTheme] = useLocalStorage('app-theme', 'light');
const toggleTheme = () => {
setTheme((prev) => (prev === 'light' ? 'dark' : 'light'));
};
return (
<div className={`p-8 ${theme === 'dark' ? 'bg-black text-white' : 'bg-white text-black'}`}>
<p>Current Theme: {theme}</p>
<button onClick={toggleTheme} className="border p-2 rounded">
Change to {theme === 'light' ? 'Dark' : 'Light'}
</button>
</div>
);
}Shopping Cart Example
Keeping items in the cart even after a page refresh.
import { useLocalStorage } from '@/hooks/use-local-storage';
export default function MiniCart() {
const [cartCount, setCartCount] = useLocalStorage('cart-count', 0);
return (
<div className="flex items-center gap-4 p-4 border rounded-lg">
<span>Items in Cart: {cartCount}</span>
<button
onClick={() => setCartCount(c => c + 1)}
className="px-4 py-2 bg-primary text-primary-foreground rounded-md"
>
Add to Cart
</button>
<button onClick={() => setCartCount(0)} className="text-destructive">
Clear
</button>
</div>
);
}API Reference
Parameters
Return Value
Storage Limits: Remember that localStorage has a size limit of roughly 5MB per origin. Avoid storing massive binary data or base64 images here.
Cross-Tab Syncing: Try opening this page in two separate windows. Updating the theme in one window will instantly update the theme in the other without a refresh!