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.