useLocalStorage

Persist state in localStorage with automatic synchronization

A React hook that syncs state with localStorage and supports cross-tab synchronization.

Usage

import { useLocalStorage } from "@your-org/react-utils";
function UserPreferences() {
const [theme, setTheme, removeTheme] = useLocalStorage("theme", "light");
return (
<div>
<p>Current theme: {theme}</p>
<button onClick={() => setTheme("dark")}>Dark Mode</button>
<button onClick={() => setTheme("light")}>Light Mode</button>
<button onClick={removeTheme}>Reset</button>
</div>
);
}

Features

  • SSR-safe (returns initial value on server)
  • Cross-tab synchronization
  • Type-safe with TypeScript generics
  • Automatic JSON serialization/deserialization
  • Error handling with fallbacks
  • Optional remove function

API Reference

Parameters

  • key (string) - The localStorage key
  • initialValue (T) - Default value if key doesn't exist

Returns

Returns a tuple with:

  • storedValue (T) - Current value from localStorage
  • setValue (function) - Update the value (supports updater function)
  • removeValue (function) - Remove the key from localStorage

Implementation

import { useState, useEffect, useCallback } from "react";
type SetValue<T> = T | ((val: T) => T);
export function useLocalStorage<T>(
key: string,
initialValue: T
): [T, (value: SetValue<T>) => void, () => void] {
// Get initial value from localStorage or use initialValue
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 loading localStorage key "${key}":`, error);
return initialValue;
}
});
// Update localStorage when value changes
const setValue = useCallback(
(value: SetValue<T>) => {
try {
const valueToStore =
value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
if (typeof window !== "undefined") {
window.localStorage.setItem(key, JSON.stringify(valueToStore));
// Dispatch custom event for cross-tab sync
window.dispatchEvent(
new CustomEvent("local-storage", {
detail: { key, value: valueToStore },
})
);
}
} catch (error) {
console.warn(`Error setting localStorage key "${key}":`, error);
}
},
[key, storedValue]
);
// Remove item from localStorage
const removeValue = useCallback(() => {
try {
if (typeof window !== "undefined") {
window.localStorage.removeItem(key);
setStoredValue(initialValue);
}
} catch (error) {
console.warn(`Error removing localStorage key "${key}":`, error);
}
}, [key, initialValue]);
// Listen for changes in other tabs/windows
useEffect(() => {
if (typeof window === "undefined") {
return;
}
const handleStorageChange = (e: StorageEvent | CustomEvent) => {
if (e instanceof StorageEvent) {
if (e.key === key && e.newValue) {
try {
setStoredValue(JSON.parse(e.newValue));
} catch (error) {
console.warn(`Error parsing localStorage key "${key}":`, error);
}
}
} else {
const { key: eventKey, value } = e.detail;
if (eventKey === key) {
setStoredValue(value);
}
}
};
window.addEventListener("storage", handleStorageChange);
window.addEventListener(
"local-storage",
handleStorageChange as EventListener
);
return () => {
window.removeEventListener("storage", handleStorageChange);
window.removeEventListener(
"local-storage",
handleStorageChange as EventListener
);
};
}, [key]);
return [storedValue, setValue, removeValue];
}

Examples

Store User Settings

interface UserSettings {
notifications: boolean;
language: string;
fontSize: number;
}
function SettingsPanel() {
const [settings, setSettings] = useLocalStorage<UserSettings>(
"user-settings",
{
notifications: true,
language: "en",
fontSize: 16,
}
);
const updateFontSize = (size: number) => {
setSettings((prev) => ({ ...prev, fontSize: size }));
};
return (
<div>
<label>
<input
type="checkbox"
checked={settings.notifications}
onChange={(e) =>
setSettings((prev) => ({
...prev,
notifications: e.target.checked,
}))
}
/>
Enable notifications
</label>
<select
value={settings.fontSize}
onChange={(e) => updateFontSize(Number(e.target.value))}>
<option value={14}>Small</option>
<option value={16}>Medium</option>
<option value={18}>Large</option>
</select>
</div>
);
}

Shopping Cart

function ShoppingCart() {
const [cart, setCart, clearCart] = useLocalStorage<CartItem[]>("cart", []);
const addToCart = (item: CartItem) => {
setCart((prev) => [...prev, item]);
};
const removeFromCart = (id: string) => {
setCart((prev) => prev.filter((item) => item.id !== id));
};
return (
<div>
<h2>Cart ({cart.length} items)</h2>
{cart.map((item) => (
<CartItem key={item.id} item={item} onRemove={removeFromCart} />
))}
<button onClick={clearCart}>Clear Cart</button>
</div>
);
}

Form Draft

function BlogPostEditor() {
const [draft, setDraft, removeDraft] = useLocalStorage("blog-draft", {
title: "",
content: "",
tags: [],
});
const handlePublish = async () => {
await publishPost(draft);
removeDraft(); // Clear draft after publishing
};
return (
<form>
<input
value={draft.title}
onChange={(e) =>
setDraft((prev) => ({ ...prev, title: e.target.value }))
}
placeholder="Title"
/>
<textarea
value={draft.content}
onChange={(e) =>
setDraft((prev) => ({ ...prev, content: e.target.value }))
}
placeholder="Content"
/>
<button type="button" onClick={handlePublish}>
Publish
</button>
</form>
);
}