useLocalStorage
Persist state in localStorage with automatic JSON serialization.
Use Cases
- User preferences (theme, language)
- Form draft saving
- Shopping cart persistence
- Recently viewed items
Code
import { useState, useEffect, useCallback } from 'react';
export function useLocalStorage<T>(
key: string,
initialValue: T
): [T, (value: T | ((prev: T) => T)) => void, () => void] {
// Get initial value from localStorage or use provided initial value
const [storedValue, setStoredValue] = useState<T>(() => {
if (typeof window === 'undefined') {
return initialValue;
}
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.warn(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
});
// Update localStorage when state changes
useEffect(() => {
if (typeof window === 'undefined') {
return;
}
try {
window.localStorage.setItem(key, JSON.stringify(storedValue));
} catch (error) {
console.warn(`Error setting localStorage key "${key}":`, error);
}
}, [key, storedValue]);
// Setter that handles both direct values and updater functions
const setValue = useCallback((value: T | ((prev: T) => T)) => {
setStoredValue((prev) => {
const nextValue = value instanceof Function ? value(prev) : value;
return nextValue;
});
}, []);
// Remove from localStorage
const removeValue = useCallback(() => {
if (typeof window === 'undefined') {
return;
}
try {
window.localStorage.removeItem(key);
setStoredValue(initialValue);
} catch (error) {
console.warn(`Error removing localStorage key "${key}":`, error);
}
}, [key, initialValue]);
return [storedValue, setValue, removeValue];
}Usage
Theme Preference
function ThemeToggle() {
const [theme, setTheme] = useLocalStorage<'light' | 'dark'>('theme', 'light');
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Current: {theme}
</button>
);
}Shopping Cart
interface CartItem {
id: string;
name: string;
quantity: number;
}
function useCart() {
const [cart, setCart, clearCart] = useLocalStorage<CartItem[]>('cart', []);
const addItem = (item: Omit<CartItem, 'quantity'>) => {
setCart((prev) => {
const existing = prev.find((i) => i.id === item.id);
if (existing) {
return prev.map((i) =>
i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i
);
}
return [...prev, { ...item, quantity: 1 }];
});
};
const removeItem = (id: string) => {
setCart((prev) => prev.filter((item) => item.id !== id));
};
return { cart, addItem, removeItem, clearCart };
}Form Draft
function ContactForm() {
const [draft, setDraft, clearDraft] = useLocalStorage('contact-draft', {
name: '',
email: '',
message: '',
});
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
await submitForm(draft);
clearDraft();
};
return (
<form onSubmit={handleSubmit}>
<input
value={draft.name}
onChange={(e) => setDraft({ ...draft, name: e.target.value })}
placeholder="Name"
/>
<input
value={draft.email}
onChange={(e) => setDraft({ ...draft, email: e.target.value })}
placeholder="Email"
/>
<textarea
value={draft.message}
onChange={(e) => setDraft({ ...draft, message: e.target.value })}
placeholder="Message"
/>
<button type="submit">Send</button>
</form>
);
}Cross-Tab Sync
Add storage event listener for cross-tab synchronization:
useEffect(() => {
const handleStorageChange = (e: StorageEvent) => {
if (e.key === key && e.newValue) {
setStoredValue(JSON.parse(e.newValue));
}
};
window.addEventListener('storage', handleStorageChange);
return () => window.removeEventListener('storage', handleStorageChange);
}, [key]);