State Persistence and Storage Integration
1. localStorage Integration with useState
| Pattern | Implementation | Persistence | Use Case |
|---|---|---|---|
| Basic localStorage | Read on mount, write on state change | Persists across sessions | User preferences, settings, themes |
| useLocalStorage hook | Custom hook wrapping useState + localStorage | Automatic sync with storage | Reusable state persistence |
| Lazy initialization | useState(() => getFromStorage()) |
Read only once on mount | Avoid reading storage on every render |
| JSON serialization | JSON.stringify/parse |
Store complex objects | Objects, arrays (not functions, undefined) |
Example: Custom useLocalStorage hook
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
// Lazy initialization - only read from localStorage once
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
// Parse stored json or return initialValue
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
});
// Update localStorage whenever state changes
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(storedValue));
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error);
}
}, [key, storedValue]);
return [storedValue, setStoredValue];
}
// Usage
function ThemeToggle() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
return (
<button onClick={toggleTheme}>
Current theme: {theme}
</button>
);
}
// Complex object storage
function UserSettings() {
const [settings, setSettings] = useLocalStorage('userSettings', {
notifications: true,
language: 'en',
fontSize: 14
});
const updateSetting = (key, value) => {
setSettings(prev => ({ ...prev, [key]: value }));
};
return (
<div>
<label>
<input
type="checkbox"
checked={settings.notifications}
onChange={(e) => updateSetting('notifications', e.target.checked)}
/>
Notifications
</label>
<select
value={settings.language}
onChange={(e) => updateSetting('language', e.target.value)}
>
<option value="en">English</option>
<option value="es">Spanish</option>
</select>
</div>
);
}
localStorage Limitations:
- Storage limit: ~5-10MB per domain (varies by browser)
- Synchronous API - can block main thread with large data
- Stores strings only - need JSON.stringify/parse for objects
- Not available in incognito/private mode in some browsers
- Can throw QuotaExceededError when full - always use try/catch
- No expiration - data persists indefinitely until cleared
2. sessionStorage Patterns for Temporary State
| Feature | localStorage | sessionStorage | Best Use |
|---|---|---|---|
| Lifetime | Persists until manually cleared | Cleared when tab/window closes | sessionStorage for temporary data |
| Scope | Shared across all tabs/windows | Isolated per tab/window | sessionStorage for tab-specific state |
| Size limit | ~5-10MB | ~5-10MB | Same limits |
| API | window.localStorage | window.sessionStorage | Same API methods |
Example: useSessionStorage for form drafts
function useSessionStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.sessionStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error('Error reading sessionStorage:', error);
return initialValue;
}
});
useEffect(() => {
try {
window.sessionStorage.setItem(key, JSON.stringify(storedValue));
} catch (error) {
console.error('Error writing sessionStorage:', error);
}
}, [key, storedValue]);
return [storedValue, setStoredValue];
}
// Usage: Save form draft during session
function ContactForm() {
const [formData, setFormData] = useSessionStorage('contactFormDraft', {
name: '',
email: '',
message: ''
});
const handleChange = (e) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
};
const handleSubmit = async (e) => {
e.preventDefault();
try {
await fetch('/api/contact', {
method: 'POST',
body: JSON.stringify(formData)
});
// Clear draft after successful submission
setFormData({ name: '', email: '', message: '' });
alert('Form submitted!');
} catch (error) {
alert('Failed to submit');
}
};
return (
<form onSubmit={handleSubmit}>
<input
name="name"
value={formData.name}
onChange={handleChange}
placeholder="Name"
/>
<input
name="email"
value={formData.email}
onChange={handleChange}
placeholder="Email"
/>
<textarea
name="message"
value={formData.message}
onChange={handleChange}
placeholder="Message"
/>
<button type="submit">Send</button>
<p>💡 Your draft is saved for this tab session</p>
</form>
);
}
// Use sessionStorage for:
// - Form drafts (don't persist across sessions)
// - Multi-step wizard progress (tab-specific)
// - Temporary filters/search state
// - Tab-specific UI state (sidebar open/closed)
3. IndexedDB Integration for Complex Data
| Storage Type | Capacity | Data Types | API Type |
|---|---|---|---|
| localStorage | ~5-10MB | Strings only (JSON serialized) | Synchronous |
| IndexedDB | ~50MB-unlimited (quota based) | Objects, Blobs, Files, Arrays, primitives | Asynchronous (Promise-based) |
Example: IndexedDB wrapper for React state
// Simple IndexedDB wrapper using idb library
import { openDB } from 'idb'; // npm install idb
// Initialize database
const dbPromise = openDB('myApp', 1, {
upgrade(db) {
if (!db.objectStoreNames.contains('state')) {
db.createObjectStore('state');
}
}
});
// IndexedDB helpers
const idbStorage = {
async getItem(key) {
return (await dbPromise).get('state', key);
},
async setItem(key, value) {
return (await dbPromise).put('state', value, key);
},
async removeItem(key) {
return (await dbPromise).delete('state', key);
},
async clear() {
return (await dbPromise).clear('state');
}
};
// Custom hook for IndexedDB state
function useIndexedDBState(key, initialValue) {
const [state, setState] = useState(initialValue);
const [isLoading, setIsLoading] = useState(true);
// Load from IndexedDB on mount
useEffect(() => {
const loadState = async () => {
try {
const storedValue = await idbStorage.getItem(key);
if (storedValue !== undefined) {
setState(storedValue);
}
} catch (error) {
console.error('Error loading from IndexedDB:', error);
} finally {
setIsLoading(false);
}
};
loadState();
}, [key]);
// Save to IndexedDB when state changes
useEffect(() => {
if (!isLoading) {
idbStorage.setItem(key, state).catch(error => {
console.error('Error saving to IndexedDB:', error);
});
}
}, [key, state, isLoading]);
return [state, setState, isLoading];
}
// Usage: Store large datasets
function ImageGallery() {
const [images, setImages, isLoading] = useIndexedDBState('galleryImages', []);
const addImage = async (file) => {
const imageData = {
id: Date.now(),
name: file.name,
blob: file, // IndexedDB can store Blob/File directly!
timestamp: new Date().toISOString()
};
setImages(prev => [...prev, imageData]);
};
if (isLoading) return <div>Loading gallery...</div>;
return (
<div>
<input
type="file"
accept="image/*"
onChange={(e) => addImage(e.target.files[0])}
/>
<div>
{images.map(img => (
<div key={img.id}>
<img src={URL.createObjectURL(img.blob)} alt={img.name} />
<p>{img.name}</p>
</div>
))}
</div>
</div>
);
}
When to Use IndexedDB:
- Large datasets (>5MB) - user-generated content, cached API responses
- Binary data - images, videos, files (Blob/File objects)
- Offline-first apps - store data for offline access
- Complex queries - supports indexes and cursors
- Use libraries:
idb,dexie,localforagefor easier API
4. State Hydration and SSR Compatibility
| Issue | Problem | Solution | Pattern |
|---|---|---|---|
| SSR mismatch | localStorage unavailable on server | Check for window object | typeof window !== 'undefined' |
| Hydration error | Server HTML ≠ client initial render | Use useEffect for storage access | Render default first, update after mount |
| Flash of content | Default state shown before storage loads | Show loading state or use script tag | Inline script before React loads |
Example: SSR-safe localStorage hook
import { useState, useEffect } from 'react';
function useLocalStorageSSR(key, initialValue) {
// Always start with initialValue on server and first client render
const [storedValue, setStoredValue] = useState(initialValue);
const [isHydrated, setIsHydrated] = useState(false);
// After hydration, read from localStorage
useEffect(() => {
// This only runs on client
try {
const item = window.localStorage.getItem(key);
if (item !== null) {
setStoredValue(JSON.parse(item));
}
} catch (error) {
console.error('Error reading localStorage:', error);
}
setIsHydrated(true);
}, [key]);
// Save to localStorage when value changes (client only)
useEffect(() => {
if (isHydrated) {
try {
window.localStorage.setItem(key, JSON.stringify(storedValue));
} catch (error) {
console.error('Error writing localStorage:', error);
}
}
}, [key, storedValue, isHydrated]);
return [storedValue, setStoredValue];
}
// Alternative: Check for window before accessing storage
function useLocalStorageWithCheck(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
// Only access localStorage if window is defined (client-side)
if (typeof window === 'undefined') {
return initialValue;
}
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
return initialValue;
}
});
useEffect(() => {
if (typeof window !== 'undefined') {
try {
window.localStorage.setItem(key, JSON.stringify(storedValue));
} catch (error) {
console.error('Error writing localStorage:', error);
}
}
}, [key, storedValue]);
return [storedValue, setStoredValue];
}
// Next.js example with no hydration mismatch
function ThemeToggle() {
const [theme, setTheme] = useLocalStorageSSR('theme', 'light');
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
// Don't render theme-dependent content until mounted
if (!mounted) {
return <div>Loading...</div>;
}
return (
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
Current theme: {theme}
</button>
);
}
SSR Hydration Issues:
- Never access
localStorageduring component render on server - Use
useEffectto read storage (runs only on client) - Return consistent initial state on server and first client render
- Show loading state or wait for mount before rendering storage-dependent UI
- For Next.js: Use dynamic imports with
ssr: falsefor storage-dependent components
5. Cross-tab State Synchronization Patterns
| Method | API | Direction | Use Case |
|---|---|---|---|
| storage event | window.addEventListener('storage') |
Unidirectional (other tabs only) | Sync localStorage changes across tabs |
| BroadcastChannel | new BroadcastChannel(name) |
Bidirectional (all tabs including sender) | Custom messages between tabs |
| SharedWorker | new SharedWorker() |
Shared state across tabs | Complex shared state management |
Example: Cross-tab sync with storage event
function useLocalStorageSync(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
// Listen for storage changes from other tabs
useEffect(() => {
const handleStorageChange = (e) => {
// Only respond to changes for this key from other tabs
if (e.key === key && e.newValue !== null) {
try {
setStoredValue(JSON.parse(e.newValue));
} catch (error) {
console.error('Error parsing storage event:', error);
}
}
};
// Storage event only fires on OTHER tabs, not the one making the change
window.addEventListener('storage', handleStorageChange);
return () => {
window.removeEventListener('storage', handleStorageChange);
};
}, [key]);
// Update localStorage and state
const setValue = (value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error('Error setting localStorage:', error);
}
};
return [storedValue, setValue];
}
// Usage: Theme syncs across all tabs
function SyncedThemeToggle() {
const [theme, setTheme] = useLocalStorageSync('theme', 'light');
return (
<div>
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
<p>Current theme: {theme}</p>
<p>💡 Open this page in multiple tabs - theme syncs across all!</p>
</div>
);
}
Example: BroadcastChannel for custom messages
function useBroadcastChannel(channelName) {
const [channel] = useState(() => new BroadcastChannel(channelName));
const [messages, setMessages] = useState([]);
useEffect(() => {
const handleMessage = (event) => {
setMessages(prev => [...prev, event.data]);
};
channel.addEventListener('message', handleMessage);
return () => {
channel.removeEventListener('message', handleMessage);
channel.close();
};
}, [channel]);
const postMessage = (data) => {
channel.postMessage(data);
};
return { messages, postMessage };
}
// Usage: Chat across tabs
function CrossTabChat() {
const { messages, postMessage } = useBroadcastChannel('chat');
const [input, setInput] = useState('');
const sendMessage = () => {
if (input.trim()) {
postMessage({
text: input,
timestamp: Date.now(),
sender: 'Tab ' + Math.random().toString(36).substr(2, 4)
});
setInput('');
}
};
return (
<div>
<div>
{messages.map((msg, i) => (
<div key={i}>
<strong>{msg.sender}:</strong> {msg.text}
</div>
))}
</div>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
/>
<button onClick={sendMessage}>Send</button>
<p>💡 Messages broadcast to all open tabs</p>
</div>
);
}
Cross-tab Sync Use Cases:
- Authentication: Logout in one tab = logout all tabs
- Shopping cart: Add item in tab A, see it in tab B
- Notifications: Mark as read in one tab, update badge in all
- Real-time collaboration: Multiple tabs editing same document
- ⚠️
storageevent doesn't fire in originating tab - need BroadcastChannel for that
6. State Backup and Recovery Strategies
| Strategy | Trigger | Storage | Purpose |
|---|---|---|---|
| Auto-save draft | Debounced state changes | localStorage/sessionStorage | Recover unsaved work after crash/reload |
| Versioned state | Major state changes | Array of snapshots | Undo/redo, state history |
| Export/Import | Manual user action | JSON file download | Backup to disk, transfer between devices |
| Cloud sync | Periodic or on change | Backend API | Cross-device sync, data safety |
Example: Auto-save with recovery
function useAutoSave(key, data, delay = 2000) {
const [lastSaved, setLastSaved] = useState(null);
const [isDirty, setIsDirty] = useState(false);
// Debounced auto-save
useEffect(() => {
setIsDirty(true);
const timer = setTimeout(() => {
try {
localStorage.setItem(key, JSON.stringify({
data,
savedAt: new Date().toISOString()
}));
setLastSaved(new Date());
setIsDirty(false);
} catch (error) {
console.error('Auto-save failed:', error);
}
}, delay);
return () => clearTimeout(timer);
}, [key, data, delay]);
return { lastSaved, isDirty };
}
// Usage: Document editor with auto-save
function DocumentEditor() {
const [content, setContent] = useState('');
const [recovered, setRecovered] = useState(false);
// Try to recover saved draft on mount
useEffect(() => {
const saved = localStorage.getItem('documentDraft');
if (saved) {
try {
const { data, savedAt } = JSON.parse(saved);
const shouldRecover = window.confirm(
`Found draft from ${new Date(savedAt).toLocaleString()}. Recover?`
);
if (shouldRecover) {
setContent(data);
setRecovered(true);
} else {
localStorage.removeItem('documentDraft');
}
} catch (error) {
console.error('Failed to recover draft:', error);
}
}
}, []);
const { lastSaved, isDirty } = useAutoSave('documentDraft', content, 2000);
const clearDraft = () => {
localStorage.removeItem('documentDraft');
setContent('');
};
return (
<div>
<textarea
value={content}
onChange={(e) => setContent(e.target.value)}
rows={10}
cols={50}
placeholder="Start typing..."
/>
<div>
{isDirty && <span>💾 Saving...</span>}
{!isDirty && lastSaved && (
<span>✓ Saved at {lastSaved.toLocaleTimeString()}</span>
)}
</div>
{recovered && <p>✓ Draft recovered</p>}
<button onClick={clearDraft}>Clear Draft</button>
</div>
);
}
Example: Export/Import state as JSON
function useStateBackup(state, filename = 'state-backup.json') {
// Export state to JSON file
const exportState = () => {
const dataStr = JSON.stringify(state, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
link.click();
URL.revokeObjectURL(url);
};
// Import state from JSON file
const importState = () => {
return new Promise((resolve, reject) => {
const input = document.createElement('input');
input.type = 'file';
input.accept = 'application/json';
input.onchange = (e) => {
const file = e.target.files[0];
const reader = new FileReader();
reader.onload = (event) => {
try {
const imported = JSON.parse(event.target.result);
resolve(imported);
} catch (error) {
reject(error);
}
};
reader.readAsText(file);
};
input.click();
});
};
return { exportState, importState };
}
// Usage: Backup app settings
function SettingsManager() {
const [settings, setSettings] = useState({
theme: 'light',
language: 'en',
notifications: true
});
const { exportState, importState } = useStateBackup(
settings,
'my-settings.json'
);
const handleImport = async () => {
try {
const imported = await importState();
setSettings(imported);
alert('Settings imported successfully!');
} catch (error) {
alert('Failed to import settings');
}
};
return (
<div>
<h3>Settings</h3>
{/* Settings UI */}
<div>
<button onClick={exportState}>Export Settings</button>
<button onClick={handleImport}>Import Settings</button>
</div>
</div>
);
}
State Persistence Summary:
- localStorage - 5-10MB, persists forever, synchronous, strings only
- sessionStorage - Tab-scoped, cleared on close, same API as localStorage
- IndexedDB - Large capacity, async, stores objects/blobs, use idb/dexie libraries
- SSR compatibility - Check typeof window, use useEffect for storage access
- Cross-tab sync - storage event for localStorage, BroadcastChannel for custom messages
- Auto-save - Debounce saves, show save status, offer draft recovery
- Export/Import - Download/upload JSON for backups and device transfer
- Error handling - Always wrap storage access in try/catch (quota errors)
- Lazy initialization - Use useState(() => getStorage()) to read only once