# State Persistence: Save App State Locally

# 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:

1. **Serialization**: Converting application state to storable format (JSON)
2. **Storage**: Saving to localStorage, sessionStorage, or IndexedDB
3. **Hydration**: Restoring state on app initialization
4. **Recovery**: Gracefully handling corrupted or missing data
5. **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

```javascript
// 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

```javascript
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

```javascript
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

```javascript
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

```javascript
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

```javascript
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**
```javascript
// 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**
```javascript
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**
```javascript
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**
```javascript
// 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**
```javascript
// Mock localStorage for tests
const localStorageMock = {
  getItem: jest.fn(),
  setItem: jest.fn(),
  removeItem: jest.fn(),
  clear: jest.fn()
};

global.localStorage = localStorageMock;
```

### 7. **Monitor Hydration**
```javascript
// 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**
```javascript
// 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.
