Complex State with useReducer

How to manage complex, interconnected state by moving the update logic into a single centralized function.

React5 min readConcept 29 of 46

What is a Reducer?

useReducer is an alternative to useState. While useState is great for simple independent values (like a boolean or a string), useReducer is designed for state objects that have multiple properties that update together.

Instead of telling React *what* the new state should be (e.g., setTasks([...tasks, newTask])), you tell React *what the user just did* by 'dispatching an action' (e.g., dispatch({ type: 'added_task', text: 'Buy milk' })).

A separate function—the 'reducer'—takes that action, applies the logic, and calculates the new state.

The Spaghettification Problem

Imagine a complex Task Manager component. It needs tasks (array), isEditing (boolean), editingId (number), and draftText (string).

If a user clicks 'Edit', you have to call setIsEditing(true), setEditingId(task.id), and setDraftText(task.text) all at once.

Your event handlers become bloated with state-updating logic. It's easy to forget one setState call and introduce bugs where the UI is in an invalid state (like isEditing being true, but editingId being null).

The Three Parts of useReducer

1. **The State:** The data itself.

2. **The Action:** A simple object describing what happened (usually with a type string).

3. **The Reducer Function:** A pure JavaScript function that takes (currentState, action) and returns the newState using a switch statement.

Because the reducer is a pure function, you can actually move it completely outside of your component, making it incredibly easy to test independently.

The Reducer Pipeline

Interact with the complex state widget below. Watch how UI interactions create 'Action' objects that flow into the centralized Reducer function to calculate the new state tree.

function reducer(state, action) { switch (action.type) { case 'added_task': return { ...state, tasks: [ ...state.tasks, { id: nextId, text: action.text } ] }; case 'deleted_task': return { ...state, tasks: state.tasks.filter( t => t.id !== action.id ) }; } }

Task Manager

No tasks yet.
UI Panel

taskReducer(state, action)

Current State Tree:
{
  "tasks": [],
  "nextId": 1
}
State Logic (Outside Component)

Notice how the UI components don't update state directly. They just 'dispatch an action' (the flying blue object). The Reducer function receives this action, evaluates it, and calculates the next state.

Check yourself

Pick an answer to lock it in, then read why. Getting one wrong is part of how it sticks.

  1. 1When is it generally recommended to use `useReducer` over `useState`?
  2. 2Which of the following is an INVALID thing to do inside a reducer function?
  3. 3In the context of `useReducer`, what is an 'Action'?

Remember this

  • useReducer centralizes complex state logic into a single function.
  • Components 'dispatch actions' instead of calling setState directly.
  • The reducer is a pure function: (state, action) => newState.
  • Reducers must NEVER mutate the current state directly.
  • Reducers must NEVER contain side effects (like fetching data).

Done with this concept?

Mark it complete to track your progress. No login needed.