State Persistence: Save App State Locally
Learn: State Persistence: Save App State Locally
Welcome to TopperBlog! 👋
I'm a tech content creator passionate about helping developers level up their careers and master cutting-edge technologies.
🎯 What I Write About:
• AI/ML Engineering & LLMs
• Web3 & Blockchain Development
• System Design & Architecture
• Interview Preparation (FAANG)
• Freelancing & Remote Work
• Modern Tech Stacks (Next.js, React, Rust, TypeScript)
• Performance Optimization & Best Practices
💼 Mission: Sharing practical, actionable insights that accelerate your tech career and maximize your earning potential.
📚 15+ In-Depth Guides covering everything from earning $10k/month as a freelancer to cracking FAANG interviews.
🌐 Let's connect and grow together in this amazing tech journey!
#TechBlogger #SoftwareEngineering #CareerGrowth #WebDevelopment #AIEngineering
State Persistence: Save App State Locally. Hydration and Recovery
Problem
Modern applications often lose critical state when users refresh the page, close the browser, or experience network interruptions. This creates a poor user experience where:
- Form data disappears mid-entry
- Shopping carts empty unexpectedly
- User preferences reset
- Authentication tokens vanish
- Scroll positions and UI state are lost
- Complex application state requires full reconstruction
Without persistence, users must restart workflows, reducing productivity and increasing frustration.
Solution
State persistence involves:
- Serialization: Converting application state to storable format (JSON)
- Storage: Saving to localStorage, sessionStorage, or IndexedDB
- Hydration: Restoring state on app initialization
- Recovery: Gracefully handling corrupted or missing data
- Synchronization: Keeping in-memory and persisted state in sync
This approach ensures seamless user experience across sessions while maintaining data integrity.
Code
1. Basic State Persistence with localStorage
// Simple persistence manager
class StateManager {
constructor(storageKey = 'appState') {
this.storageKey = storageKey;
this.state = {};
this.listeners = [];
}
// Save state to localStorage
persist(state) {
try {
const serialized = JSON.stringify(state);
localStorage.setItem(this.storageKey, serialized);
this.state = state;
} catch (error) {
console.error('Failed to persist state:', error);
}
}
// Load state from localStorage
hydrate() {
try {
const stored = localStorage.getItem(this.storageKey);
if (stored) {
this.state = JSON.parse(stored);
return this.state;
}
} catch (error) {
console.error('Failed to hydrate state:', error);
this.clear();
}
return null;
}
// Subscribe to state changes
subscribe(listener) {
this.listeners.push(listener);
return () => {
this.listeners = this.listeners.filter(l => l !== listener);
};
}
// Notify listeners
notify() {
this.listeners.forEach(listener => listener(this.state));
}
// Clear persisted state
clear() {
localStorage.removeItem(this.storageKey);
this.state = {};
}
}
// Usage
const stateManager = new StateManager('myApp');
stateManager.hydrate();
const appState = stateManager.state || {
user: null,
cart: [],
theme: 'light'
};
// Update and persist
appState.user = { id: 1, name: 'John' };
stateManager.persist(appState);
stateManager.notify();
2. React Hook for State Persistence
import { useState, useEffect, useCallback } from 'react';
function usePersistedState(key, initialValue) {
// Initialize state from localStorage or use initial value
const [state, setState] = useState(() => {
try {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
} catch (error) {
console.error(`Failed to load ${key}:`, error);
return initialValue;
}
});
// Persist state whenever it changes
useEffect(() => {
try {
localStorage.setItem(key, JSON.stringify(state));
} catch (error) {
console.error(`Failed to persist ${key}:`, error);
}
}, [key, state]);
return [state, setState];
}
// Usage in component
function ShoppingCart() {
const [cart, setCart] = usePersistedState('cart', []);
const [user, setUser] = usePersistedState('user', null);
const addItem = (item) => {
setCart([...cart, item]);
};
return (
<div>
<h1>Cart ({cart.length} items)</h1>
<button onClick={() => addItem({ id: 1, name: 'Product' })}>
Add Item
</button>
</div>
);
}
3. Advanced Hydration with Validation
class RobustStateManager {
constructor(storageKey, schema) {
this.storageKey = storageKey;
this.schema = schema; // Validation schema
this.state = {};
}
// Validate state against schema
validate(state) {
if (!this.schema) return true;
for (const [key, validator] of Object.entries(this.schema)) {
if (!(key in state)) {
console.warn(`Missing required key: ${key}`);
return false;
}
if (!validator(state[key])) {
console.warn(`Invalid value for ${key}`);
return false;
}
}
return true;
}
// Hydrate with recovery
hydrate(defaultState) {
try {
const stored = localStorage.getItem(this.storageKey);
if (!stored) {
this.state = defaultState;
return this.state;
}
const parsed = JSON.parse(stored);
// Validate and recover
if (this.validate(parsed)) {
this.state = parsed;
} else {
console.warn('Stored state invalid, using defaults');
this.state = defaultState;
this.persist(defaultState);
}
return this.state;
} catch (error) {
console.error('Hydration failed:', error);
this.state = defaultState;
return this.state;
}
}
persist(state) {
try {
if (!this.validate(state)) {
throw new Error('Invalid state');
}
localStorage.setItem(this.storageKey, JSON.stringify(state));
this.state = state;
} catch (error) {
console.error('Persistence failed:', error);
}
}
clear() {
localStorage.removeItem(this.storageKey);
this.state = {};
}
}
// Usage with schema
const schema = {
user: (val) => val === null || (typeof val === 'object' && 'id' in val),
cart: (val) => Array.isArray(val),
theme: (val) => ['light', 'dark'].includes(val)
};
const manager = new RobustStateManager('appState', schema);
const state = manager.hydrate({
user: null,
cart: [],
theme: 'light'
});
4. IndexedDB for Large State
class IndexedDBStateManager {
constructor(dbName, storeName) {
this.dbName = dbName;
this.storeName = storeName;
this.db = null;
}
// Initialize database
async init() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, 1);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve(this.db);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(this.storeName)) {
db.createObjectStore(this.storeName);
}
};
});
}
// Save state
async persist(key, state) {
const transaction = this.db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
return new Promise((resolve, reject) => {
const request = store.put(state, key);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve();
});
}
// Load state
async hydrate(key) {
const transaction = this.db.transaction([this.storeName], 'readonly');
const store = transaction.objectStore(this.storeName);
return new Promise((resolve, reject) => {
const request = store.get(key);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}
// Clear all
async clear() {
const transaction = this.db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
return new Promise((resolve, reject) => {
const request = store.clear();
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve();
});
}
}
// Usage
const idbManager = new IndexedDBStateManager('myApp', 'state');
await idbManager.init();
await idbManager.persist('appState', largeState);
const restored = await idbManager.hydrate('appState');
5. Redux with Persistence Middleware
import { createStore, applyMiddleware } from 'redux';
// Persistence middleware
const persistenceMiddleware = (storageKey) => (store) => (next) => (action) => {
const result = next(action);
// Persist after every action
try {
const state = store.getState();
localStorage.setItem(storageKey, JSON.stringify(state));
} catch (error) {
console.error('Failed to persist:', error);
}
return result;
};
// Reducer
function appReducer(state = {}, action) {
switch (action.type) {
case 'ADD_ITEM':
return { ...state, items: [...state.items, action.payload] };
case 'HYDRATE':
return action.payload;
default:
return state;
}
}
// Create store with persistence
const store = createStore(
appReducer,
applyMiddleware(persistenceMiddleware('appState'))
);
// Hydrate on app start
function hydrateStore() {
try {
const stored = localStorage.getItem('appState');
if (stored) {
store.dispatch({
type: 'HYDRATE',
payload: JSON.parse(stored)
});
}
} catch (error) {
console.error('Hydration failed:', error);
}
}
hydrateStore();
6. Selective Persistence
class SelectiveStateManager {
constructor(storageKey, persistenceConfig) {
this.storageKey = storageKey;
this.persistenceConfig = persistenceConfig; // { key: boolean }
}
// Persist only selected keys
persist(fullState) {
const stateToSave = {};
for (const [key, shouldPersist] of Object.entries(this.persistenceConfig)) {
if (shouldPersist && key in fullState) {
stateToSave[key] = fullState[key];
}
}
try {
localStorage.setItem(this.storageKey, JSON.stringify(stateToSave));
} catch (error) {
console.error('Persistence failed:', error);
}
}
// Hydrate and merge with defaults
hydrate(defaultState) {
try {
const stored = localStorage.getItem(this.storageKey);
if (stored) {
const persisted = JSON.parse(stored);
return { ...defaultState, ...persisted };
}
} catch (error) {
console.error('Hydration failed:', error);
}
return defaultState;
}
}
// Usage
const config = {
user: true, // Persist user
cart: true, // Persist cart
theme: true, // Persist theme
notifications: false, // Don't persist
tempData: false // Don't persist
};
const manager = new SelectiveStateManager('appState', config);
const state = manager.hydrate(defaultState);
Tips
1. Storage Limits
- localStorage: ~5-10MB per domain
- sessionStorage: ~5-10MB per tab
- IndexedDB: 50MB+ (quota-based)
- Use IndexedDB for large datasets
2. Security Considerations
// Never store sensitive data in localStorage
// ❌ Don't do this:
localStorage.setItem('password', userPassword);
// ✅ Do this:
// Store only non-sensitive identifiers, use secure cookies for auth
localStorage.setItem('userId', user.id);
3. Versioning State
const persistedState = {
version: 1,
data: { /* state */ }
};
// On hydration, check version and migrate if needed
if (stored.version < currentVersion) {
stored.data = migrateState(stored.data);
}
4. Debounce Persistence
function useDebouncedPersistence(state, key, delay = 1000) {
useEffect(() => {
const timer = setTimeout(() => {
localStorage.setItem(key, JSON.stringify(state));
}, delay);
return () => clearTimeout(timer);
}, [state, key, delay]);
}
5. Error Handling
// Handle quota exceeded
try {
localStorage.setItem(key, value);
} catch (e) {
if (e.name === 'QuotaExceededError') {
console.error('Storage quota exceeded');
// Clear old data or use IndexedDB
}
}
6. Testing Persistence
// Mock localStorage for tests
const localStorageMock = {
getItem: jest.fn(),
setItem: jest.fn(),
removeItem: jest.fn(),
clear: jest.fn()
};
global.localStorage = localStorageMock;
7. Monitor Hydration
// Track hydration success
const hydrateWithMetrics = (key, defaultState) => {
const startTime = performance.now();
const state = hydrate(key, defaultState);
const duration = performance.now() - startTime;
console.log(`Hydration took ${duration}ms`);
return state;
};
8. Cross-Tab Synchronization
// Sync state across browser tabs
window.addEventListener('storage', (event) => {
if (event.key === 'appState') {
const newState = JSON.parse(event.newValue);
updateAppState(newState);
}
});
State persistence is essential for modern applications. Choose storage based on data size, implement robust validation, handle errors gracefully, and always consider security implications.