Quiz

What is the `useReducer` hook in React and when should it be used?

Topics
React

TL;DR

The useReducer hook in React is used for managing complex state logic in functional components. It is an alternative to useState and is particularly useful when the state has multiple sub-values or when the next state depends on the previous one. It takes a reducer function and an initial state as arguments and returns the current state and a dispatch function.

const [state, dispatch] = useReducer(reducer, initialState);

Use useReducer when you have complex state logic that involves multiple sub-values or when the next state depends on the previous state.


What is the useReducer hook in React and when should it be used?

Introduction to useReducer

The useReducer hook is a React hook that is used for managing state in functional components. It is an alternative to the useState hook and is particularly useful for managing more complex state logic. The useReducer hook is similar to the reduce function in JavaScript arrays, where you have a reducer function that determines how the state should change in response to actions.

Syntax

The useReducer hook takes two arguments: a reducer function and an initial state. It returns an array with the current state and a dispatch function.

const [state, dispatch] = useReducer(reducer, initialState);

Reducer function

The reducer function is a pure function that takes the current state and an action as arguments and returns the new state. The action is an object that typically has a type property and an optional payload.

function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error('Unhandled action: ' + action.type);
}
}

Example usage

Here is a simple example of using useReducer to manage a counter state:

import React, { useReducer } from 'react';
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment':
return { count: state.count + 1 };
case 'decrement':
return { count: state.count - 1 };
default:
throw new Error('Unhandled action: ' + action.type);
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<div>
<p>Count: {state.count}</p>
<button onClick={() => dispatch({ type: 'increment' })}>Increment</button>
<button onClick={() => dispatch({ type: 'decrement' })}>Decrement</button>
</div>
);
}
export default Counter;

Lazy initialization

useReducer accepts an optional third argument: an init function. When provided, React calls init(initialArg) once on mount and uses the result as the initial state. This is useful when computing the initial state is expensive, or when you want to derive it from a prop.

function init(initialCount) {
return { count: initialCount };
}
function Counter({ initialCount }) {
const [state, dispatch] = useReducer(reducer, initialCount, init);
// ...
}

Bailing out of an update

If the reducer returns the exact same value (compared with Object.is) as the current state, React bails out — no re-render is scheduled and no children re-render. This is one reason reducers must be pure and must not mutate the existing state object: a mutated-but-same-reference return looks like a bailout to React, but the state has actually changed.

When to use useReducer

  • Complex state transitions: Use useReducer when state updates involve multiple sub-values or when the next state depends on the previous one in non-trivial ways. Centralizing the transitions in a reducer is easier to reason about than scattering several useState setters.
  • Explicit, named actions: Dispatching { type: 'increment' } documents intent at the call site; the reducer is the single place that knows how to apply each action.
  • Testability: Because reducers are pure functions of (state, action) -> state, they can be unit-tested in isolation without rendering any components.
  • Stable dispatch identity: React guarantees that the dispatch function has a stable identity across renders. You can safely include it in useEffect/useCallback dependency arrays (or omit it) without causing effects to re-run, and you can pass it down through context without invalidating memoized children.

A common misconception is that useReducer inherently reduces re-renders compared to useState. It does not — both schedule a render whenever the state reference changes. The real wins are around how you structure and reason about updates, plus the stable dispatch identity.

Further reading

Edit on GitHub