Back to patterns

State Management

STATE

A visual approach to debugging state management issues, tracking component re-renders, state mutations, and cache invalidation patterns.

Diagram for State Management

Use Cases

  • Complex state flows
  • Component re-renders
  • Cache invalidation
  • State mutation tracking

Production Implementation

Implementation
// Real-world example: Type-safe state management with debugging and performance optimization
import { createContext, useContext, useCallback, useRef, useEffect } from 'react';

// Type definitions for state store
type Listener<T> = (state: T) => void;
type Selector<T, R> = (state: T) => R;
type Action<T> = (state: T) => Partial<T>;
type Middleware<T> = (action: Action<T>, state: T) => Promise<void> | void;

interface StoreConfig<T> {
  initialState: T;
  middleware?: Middleware<T>[];
  persist?: boolean;
  debug?: boolean;
}

class Store<T extends object> {
  private state: T;
  private listeners: Set<Listener<T>>;
  private middleware: Middleware<T>[];
  private debug: boolean;
  private persist: boolean;
  private updateCount: number;
  private lastUpdate: number;

  constructor(config: StoreConfig<T>) {
    this.state = this.loadInitialState(config);
    this.listeners = new Set();
    this.middleware = config.middleware || [];
    this.debug = config.debug || false;
    this.persist = config.persist || false;
    this.updateCount = 0;
    this.lastUpdate = Date.now();
  }

  private loadInitialState(config: StoreConfig<T>): T {
    if (config.persist && typeof window !== 'undefined') {
      const stored = localStorage.getItem('app_state');
      if (stored) {
        try {
          return JSON.parse(stored);
        } catch (e) {
          console.error('Failed to load persisted state:', e);
        }
      }
    }
    return config.initialState;
  }

  getState(): T {
    return this.state;
  }

  async dispatch(action: Action<T>): Promise<void> {
    const startTime = performance.now();
    const prevState = { ...this.state };
    
    try {
      // Run middleware before state update
      for (const middleware of this.middleware) {
        await middleware(action, prevState);
      }

      // Update state
      const update = action(prevState);
      const nextState = { ...prevState, ...update };
      
      // Validate state changes
      this.validateStateUpdate(prevState, nextState);
      
      this.state = nextState;
      this.updateCount++;
      this.lastUpdate = Date.now();

      // Persist if enabled
      if (this.persist) {
        this.persistState();
      }

      // Notify listeners
      this.notifyListeners();

      // Debug logging
      if (this.debug) {
        this.logStateUpdate(prevState, nextState, startTime);
      }

    } catch (error) {
      console.error('State update failed:', error);
      throw error;
    }
  }

  private validateStateUpdate(prev: T, next: T): void {
    // Detect accidental mutations
    if (Object.keys(next).some(key => prev[key] === next[key] && typeof prev[key] === 'object')) {
      console.warn('Possible state mutation detected. State updates should be immutable.');
    }

    // Check for undefined values
    Object.entries(next).forEach(([key, value]) => {
      if (value === undefined) {
        console.warn(`State key "${key}" was set to undefined. This may cause issues.`);
      }
    });
  }

  private persistState(): void {
    try {
      localStorage.setItem('app_state', JSON.stringify(this.state));
    } catch (e) {
      console.error('Failed to persist state:', e);
    }
  }

  private logStateUpdate(prev: T, next: T, startTime: number): void {
    const duration = performance.now() - startTime;
    const changes = Object.keys(next).filter(key => prev[key] !== next[key]);
    
    console.group('State Update');
    console.log('Previous:', prev);
    console.log('Next:', next);
    console.log('Changed keys:', changes);
    console.log(`Update took ${duration.toFixed(2)}ms`);
    console.log(`Total updates: ${this.updateCount}`);
    console.groupEnd();
  }

  subscribe(listener: Listener<T>): () => void {
    this.listeners.add(listener);
    return () => this.listeners.delete(listener);
  }

  private notifyListeners(): void {
    for (const listener of this.listeners) {
      listener(this.state);
    }
  }

  // Debug utilities
  getDebugInfo() {
    return {
      updateCount: this.updateCount,
      lastUpdate: new Date(this.lastUpdate).toISOString(),
      listenerCount: this.listeners.size,
      stateSize: JSON.stringify(this.state).length,
    };
  }
}

// React integration
const StoreContext = createContext<Store<any> | null>(null);

export function useStore<T extends object, R>(
  selector: Selector<T, R>,
  equalityFn: (a: R, b: R) => boolean = Object.is
): R {
  const store = useContext(StoreContext);
  if (!store) throw new Error('Store not found in context');

  const [state, setState] = useState(selector(store.getState()));
  const prevValue = useRef(state);

  useEffect(() => {
    return store.subscribe((newState) => {
      const newValue = selector(newState);
      if (!equalityFn(prevValue.current, newValue)) {
        prevValue.current = newValue;
        setState(newValue);
      }
    });
  }, [store, selector, equalityFn]);

  return state;
}

// Example usage with TypeScript
interface AppState {
  user: {
    id: string;
    name: string;
    preferences: {
      theme: 'light' | 'dark';
      notifications: boolean;
    };
  } | null;
  todos: Array<{
    id: string;
    text: string;
    completed: boolean;
  }>;
  ui: {
    sidebarOpen: boolean;
    activeModal: string | null;
  };
}

// Middleware example: Logger
const loggerMiddleware: Middleware<AppState> = async (action, state) => {
  console.group('Action');
  console.log('Previous State:', state);
  console.log('Action:', action.name);
  console.groupEnd();
};

// Middleware example: Analytics
const analyticsMiddleware: Middleware<AppState> = async (action, state) => {
  if (action.name === 'updateUser') {
    analytics.track('user_updated', {
      userId: state.user?.id,
      timestamp: new Date().toISOString()
    });
  }
};

// Create store instance
const store = new Store<AppState>({
  initialState: {
    user: null,
    todos: [],
    ui: {
      sidebarOpen: false,
      activeModal: null
    }
  },
  middleware: [loggerMiddleware, analyticsMiddleware],
  persist: true,
  debug: process.env.NODE_ENV === 'development'
});

// Component example
function UserProfile() {
  const user = useStore((state: AppState) => state.user);
  const updateUser = useCallback((name: string) => {
    store.dispatch((state) => ({
      user: state.user ? { ...state.user, name } : null
    }));
  }, []);

  if (!user) return <div>Please log in</div>;

  return (
    <div>
      <h1>{user.name}</h1>
      <button onClick={() => updateUser('New Name')}>
        Update Name
      </button>
    </div>
  );
}

Code Examples

Common State Management Issues

typescript
// ❌ Common state management issues
class Component {
  // Issue 1: Direct state mutation
  updateUser(name: string) {
    this.state.user.name = name;
  }
  
  // Issue 2: No state immutability
  addTodo(todo: Todo) {
    this.state.todos.push(todo);
  }
  
  // Issue 3: Inconsistent updates
  async fetchUser() {
    const user = await api.getUser();
    this.state.user = user;
    // Preferences might be out of sync
  }
  
  // Issue 4: No cleanup
  componentDidMount() {
    window.addEventListener('resize', this.handleResize);
  }
}

Common issues include direct state mutations, lack of immutability, inconsistent updates, and missing cleanup.

Robust State Management

typescript
// ✅ Robust state management
class Component {
  // Immutable state updates
  updateUser(name: string) {
    this.setState(state => ({
      user: state.user
        ? { ...state.user, name }
        : null
    }));
  }
  
  // Batch related updates
  async fetchUserWithPreferences() {
    const [user, preferences] = await Promise.all([
      api.getUser(),
      api.getUserPreferences()
    ]);
    
    this.setState({
      user: { ...user, preferences }
    });
  }
  
  // Proper cleanup
  componentDidMount() {
    const handler = this.handleResize.bind(this);
    window.addEventListener('resize', handler);
    
    return () => {
      window.removeEventListener('resize', handler);
    };
  }
  
  // Optimized re-renders
  shouldComponentUpdate(nextProps, nextState) {
    return !isEqual(this.state, nextState) ||
           !isEqual(this.props, nextProps);
  }
}

This implementation uses immutable updates, batches related changes, includes proper cleanup, and optimizes re-renders.

Best Practices

  • Use immutable state updates
  • Implement proper type safety
  • Batch related state changes
  • Add debugging capabilities
  • Include state persistence
  • Optimize component re-renders
  • Handle cleanup properly

Common Pitfalls

  • Direct state mutations
  • Missing type safety
  • Inconsistent state updates
  • No performance optimization
  • Memory leaks from missing cleanup
  • Poor error handling
  • Lack of debugging tools