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, localforage for 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 localStorage during component render on server
  • Use useEffect to 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: false for 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
  • ⚠️ storage event 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