React useReducer Hook: Managing Complex State

React useReducer Hook: Managing Complex State

useReducer is ideal when your state logic is complex, involves multiple sub-values, or depends on previous state. Learn its syntax, roles, and practical points.

frontend
September 2, 2025
7 min read

Introduction: Why useReducer?

The useReducer hook in React centralizes complex state logic, helping when updates rely on previous state or when state structure is non-trivial. It's often used for forms, undo operations, or when state transitions are numerous.

What is useReducer? Why Use It

If your state is basic (a single value), useState is fine. But if state needs coordinated changes (like a shopping cart, multi-step form, or undo/redo), useReducer keeps all logic in one place with actions and a reducer function.

✅ Feature Comparison Table

FeatureuseStateuseReducer
Simple logic
Complex logic
Multiple fields
State depends on previous
Encapsulated logic

useReducer Syntax & Parameters

js
const [state, dispatch] = useReducer(reducer, initialArg, init);
  • state: Current state value, managed by the reducer.
  • dispatch: Function to send action objects to the reducer.
  • reducer: Pure function that takes (state, action) and returns next state.
  • initialArg: Starting state value, any type.
  • init (optional): Lazy initializer for computing initial state (runs only once).

Analogy: Dispatching in Real Life

Imagine a dispatcher in a warehouse: they shout out an action (e.g., "add item"), the manager (reducer) checks the order and updates the inventory (state) accordingly.

Code Example: Complex Form State

js
const initialState = { name: '', age: '' };
function reducer(state, action) {
  switch (action.type) {
    case 'SET_NAME': return { ...state, name: action.payload };
    case 'SET_AGE': return { ...state, age: action.payload };
    default: throw new Error();
  }
}
const [state, dispatch] = useReducer(reducer, initialState);

Explanation

This reducer manages multiple form fields in a single object, making it much easier to maintain than separate useState calls.

Advanced: Lazy Initialization

js
function init(arg) { return { count: arg }; }
function reducer(state, action) {
  switch(action.type) {
    case 'increment': return { count: state.count + 1 };
    case 'reset': return init(action.payload);
    default: return state;
  }
}
const [state, dispatch] = useReducer(reducer, 0, init);

Use Case:

By passing init, you can compute the initial state once on mount, or reset to a new value efficiently, such as from props or localStorage.

Best Practices

  • Keep reducer pure (no async or side-effects inside).
  • Use action objects with type and payload for flexibility.
  • Avoid direct state mutations; always return a new state object.
  • Break out reducer function for clarity and reusability.
  • Use lazy initializer for performance when computing expensive initial state.

Comparison: useReducer vs Redux

Both useReducer and Redux use reducer functions, actions, and dispatch, but Redux is for global app state, while useReducer is for local, component state.

Real-World Example: Shopping List

js
function shoppingReducer(state, action) {
  switch(action.type) {
    case 'ADD_ITEM': return [...state, action.payload];
    case 'REMOVE_ITEM': return state.filter((item) => item.id !== action.payload);
    default: return state;
  }
}
const [items, dispatch] = useReducer(shoppingReducer, []);

Use Case:

This manages a dynamic list with complex add/removal logic, scaling much better than useState as your app grows.

📊 Table: Key Concepts

PropRoleBest Practice
stateCurrent valueReturn new object, never mutate
dispatchSends actionsAlways use React dispatch
reducerUpdates statePure logic only
initialArgInitial valuePass object/array for complex state
initLazy setupUse for optimized initialization