Advanced React State: The `useReducer` Hook

Tame complex state logic and build more robust, predictable components with React's powerful `useReducer` hook.

Lesson ProgressStep 1 of 9
State
{ count: 0 }
Action
{ type: 'INC' }
Reducer
function(state, action)
New State
{ count: 1 }
0 EXP

Hello! Let's explore useReducer. First, here's a familiar counter using useState.

const [count, setCount] = useState(0);
<button onClick={() => setCount(count + 1)}>{count}</button>

Why Bother with `useReducer`?

`useState` is great for simple state (like a boolean, number, or string). However, when your state becomes more complex, `useReducer` is a better choice.

Consider `useReducer` when:

  • Your state is a complex object or array.
  • The next state value depends on the previous one.
  • You have multiple state values that are all related and change together.
  • You want to move the state logic *out* of your component to make it cleaner and easier to test.
// Too complex for useState:
const [loading, setLoading] = useState(false);
const [data, setData] = useState(null);
const [error, setError] = useState(null);

System Check

When is useReducer generally preferred over useState?

Advanced Holo-Simulations

0 EXP

Log in to unlock these advanced training modules and test your skills.


Achievements

🏆
Reducer Master

Successfully build a component managed by useReducer.

🏗️
Action Architect

Correctly structure and dispatch actions with payloads.

✍️
State Sentinel

Properly order the useReducer logic flow.

Mission: Build a Reducer Counter

Complete the `Counter` component. It needs a reducer that handles `'INCREMENT'` and `'DECREMENT'` actions. For an extra challenge, add a `'SET_VALUE'` action that uses a `payload`.

A.D.A. Feedback:

> System integrity looks stable. Code is valid.

Challenge: Order the Logic

Drag the pieces of code into the logical order they would appear or be executed in a typical `useReducer` setup.

const [state, dispatch] = useReducer(reducer, initialState);
function reducer(state, action) { ... }
dispatch({ type: 'MY_ACTION' });

Challenge: Complete the Reducer

Fill in the missing parts of this reducer function that handles adding an item to a list.

function reducer(state, action) {
switch () {
case 'ADD_ITEM':
return [...state,];
default:
return;
}}

Consult A.D.A.

Community Holo-Net

Peer Project Review

Submit your "Reducer Counter" project for feedback from other Net-Runners.

The `useReducer` Playbook: From Hook to Hero

The `useReducer` hook is often introduced as "the other" state management hook in React, a complex alternative to the simple and familiar `useState`. While `useState` is perfect for simple, independent state values (like a boolean toggle or a single input field), `useReducer` is a specialized tool for taming **state complexity**.

Understanding *when* and *how* to deploy `useReducer` is what separates a good React developer from a great one. It's not just about managing state; it's about making state transitions **predictable, testable, and scalable**.

The Tipping Point: When to Switch from `useState`

You don't need `useReducer` for everything. But you should strongly consider it when you notice these patterns:

  • Complex State Objects: Your state is an object or array with multiple properties that often change together.
    const [state, setState] = useState({ loading: true, data: null, error: null });
  • Interdependent State: Updating one piece of state requires logic based on another piece of state. (e.g., `setCount(prevCount => prevCount + 1)` is a simple version, but imagine it being more complex).
  • Cascading Updates: A single event triggers a complex series of state updates. With `useState`, this can lead to messy `useEffect` hooks or long, confusing event handlers.
  • Logic Sharing: You want to extract and test your state logic in isolation from your component, or reuse the same logic in multiple components.

The Core Pattern: Centralizing Logic

The magic of `useReducer` is that it **decouples** the *intent* to change state from the *implementation* of the state change.

❌ With `useState`

function handleClick() {
  setLoading(true);
  setError(null);
  fetchData(id).then(res => {
    setData(res.data);
    setLoading(false);
  }).catch(err => {
    setError(err);
    setLoading(false);
  });
}

The component is cluttered with logic, detailing *how* to set each state.

✔️ With `useReducer`

function handleClick() {
  dispatch({ type: 'FETCH_START' });
  fetchData(id).then(res => {
    dispatch({ type: 'FETCH_SUCCESS', payload: res.data });
  }).catch(err => {
    dispatch({ type: 'FETCH_ERROR', payload: err });
  });
}

The component only describes *what* happened by dispatching actions. The *how* is neatly hidden inside the reducer.

Advanced Patterns: Beyond the Basics

`useReducer` unlocks powerful patterns, especially when combined with other React features.

  • Lazy Initialization: You can pass an `init` function as the third argument to `useReducer`. React will only call this function once on the initial render to calculate the initial state. This is useful for computationally expensive initial state.
    const [state, dispatch] = useReducer(reducer, initialArg, createInitialState);
  • Global State with Context: This is the most powerful pattern. By providing both `state` and `dispatch` in a React Context, you can give any component in the tree the ability to read the global state and dispatch actions, all without prop-drilling. This provides a lightweight, built-in alternative to Redux for many applications.
Key Takeaway: Treat `useReducer` as your go-to tool for complex, co-located state. It centralizes your logic, makes your components more declarative, and improves testability. The reducer function itself is a pure JavaScript function, meaning you can export it and unit-test it completely independent of React.

`useReducer` Glossary

`useReducer`
A built-in React Hook that accepts a reducer function and an initial state, returning the current state and a `dispatch` function. It is an alternative to `useState` for managing complex state logic.
Reducer
A **pure function** that takes two arguments: the current `state` and an `action` object. It computes and returns the **new state**. It should *never* mutate the original state.
Action
A plain JavaScript object that describes *what happened*. It must have a `type` property (usually a string) and can optionally contain extra data, conventionally called a `payload`.
Dispatch
The function returned by `useReducer` that you call to "dispatch" or send an action object to the reducer. This is the *only* way to trigger a state update when using `useReducer`.
Payload
The conventional name for the property on an action object that holds any data needed to compute the new state. For example, `action: { { 'SET_USER', payload: { { id: 1, name: 'Alice' }; } } }`.
Pure Function
A function that, given the same inputs, will *always* return the same output and has no side effects (like API calls or direct DOM manipulation). Reducers *must* be pure functions.
Lazy Initialization
An optional third argument to `useReducer`. It's a function that React runs once on the initial render to calculate the initial state. This is used to avoid re-running expensive calculations on every render.

About the Author

Author's Avatar

TodoTutorial Team

Passionate developers and educators making programming accessible to everyone.

This article was written and reviewed by our team of senior React developers, who use hooks like `useReducer` daily to build robust, scalable web applications.

Verification and Updates

Last reviewed: November 2025.

We strive to keep our content accurate and up-to-date. This tutorial is based on the latest React documentation (React 18+) and industry best practices.

External Resources

Found an error or have a suggestion? Contact us!