Frontend Patterns

Pattern

State Reducer

Use reducer functions to handle complex state transitions with predictable logic.

State Management Intermediate

State Reducer

Problem

Complex state transitions scattered across multiple state declarations become impossible to reason about. Related state updates happen in different event handlers, leading to impossible states and race conditions. Debugging state changes requires hunting through numerous state update calls, and there’s no single source of truth for what transitions are valid.

Solution

Manage complex state transitions through a reducer function that handles actions. This centralizes update logic and makes state changes predictable and testable.

Example

This demonstrates managing complex state transitions through a reducer function that centralizes update logic, making state changes predictable, testable, and easier to debug.

// Framework-agnostic reducer pattern
function createReducer(reducer, initialState) {
  let state = initialState;
  const listeners = [];

  return {
    getState: () => state,
    dispatch: (action) => {
      state = reducer(state, action);  // All updates go through reducer
      listeners.forEach(listener => listener(state));  // Notify subscribers
    },
    subscribe: (listener) => {
      listeners.push(listener);
      // Return unsubscribe function
      return () => {
        const index = listeners.indexOf(listener);
        listeners.splice(index, 1);
      };
    }
  };
}

// Usage - define valid state transitions
function reducer(state, action) {
  switch (action.type) {
    case 'increment': return { count: state.count + 1 };
    case 'decrement': return { count: state.count - 1 };
    default: return state;  // Unknown actions return current state
  }
}

const store = createReducer(reducer, { count: 0 });
store.subscribe(state => console.log(state));  // React to changes
store.dispatch({ type: 'increment' });         // Dispatch action

Benefits

  • Centralizes state update logic in one testable function.
  • Makes state transitions explicit and predictable.
  • Prevents impossible states by controlling valid transitions.
  • Simplifies debugging by providing clear action history.

Tradeoffs

  • Adds boilerplate with action types and reducer switch statements.
  • Can be overkill for simple state that doesn’t need complex transitions.
  • Makes simple updates more verbose than direct useState calls.
  • Requires understanding reducer pattern and action dispatching.
Stay Updated

Get New Patterns
in Your Inbox

Join thousands of developers receiving weekly insights on frontend architecture patterns

No spam. Unsubscribe anytime.