🚀 LEVEL UP TO SENIOR:Unlock 500+ Advanced Practical Challenges & Exercises.
🎓 COURSERA PARTNER:Earn professional Google, Meta, and IBM certificates to supercharge your resume.
REFERENCEreact

react Documentation

LOADING ENGINE...

useReducer

AI & DATA SCIENCE // usereducer

useReducer manages more complex component state by describing updates as dispatched actions handled by a single reducer function, similar to Redux's pattern but local to a component.

Syntax

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

Deep Dive Course

useReducer takes a reducer function, (state, action) => newState, and an initial state, returning the current state and a dispatch function used to trigger updates by sending an action object, typically with a type field describing what happened. Rather than scattering multiple related useState calls and update logic across a component, useReducer centralizes all the state transition logic into one single reducer function, which is especially useful when a piece of state has several related sub-values that tend to update together, or when the next state genuinely depends on complex logic based on the previous state and the specific action that occurred.

1Understanding useReducer

useReducer takes a reducer function, (state, action) => newState, and an initial state, returning the current state and a dispatch function used to trigger updates by sending an action object, typically with a type field describing what happened. Rather than scattering multiple related useState calls and update logic across a component, useReducer centralizes all the state transition logic into one single reducer function, which is especially useful when a piece of state has several related sub-values that tend to update together, or when the next state genuinely depends on complex logic based on the previous state and the specific action that occurred.

💡

Reach for useReducer over several separate useState calls specifically when state updates involve complex logic, several related sub-values that change together, or when the exact same reducer logic could reasonably be tested independently of any particular component.

editor.html
import { useReducer } from 'react';

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

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  return (
    <>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
    </>
  );
}
localhost:3000

2Practical Example

Here is a real-world application of useReducer showing how it is used in production React code.

editor.html
function formReducer(state, action) {
  switch (action.type) {
    case 'field_changed':
      return { ...state, [action.field]: action.value };
    case 'reset':
      return { name: '', email: '' };
    default:
      return state;
  }
}

const [form, dispatch] = useReducer(formReducer, { name: '', email: '' });
dispatch({ type: 'field_changed', field: 'name', value: 'Ana' });
localhost:3000

3Best Practices

Follow these guidelines when working with useReducer:

1. Use useReducer instead of multiple related useState calls when several pieces of state tend to update together in response to the same events

2. Keep the reducer function pure, computing and returning a new state object based only on its state and action arguments, with no side effects performed inside it

3. Give dispatched action objects a clear, descriptive type field, and any needed extra data as additional properties, making the reducer's switch/if-else logic easy to follow

⚠️

Tip: Reach for useReducer over several separate useState calls specifically when state updates involve complex logic, several related sub-values that change together, or when the exact same reducer logic could reasonably be tested independently of any particular component.

editor.html
import { useReducer } from 'react';

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

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  return (
    <>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
    </>
  );
}
localhost:3000

4Lazy Initialization

useReducer accepts an optional third argument, an init function: useReducer(reducer, initialArg, init). React calls init(initialArg) exactly once, on the first render, to compute the actual initial state — useful when that computation is expensive or depends on props, since it avoids re-running the logic on every render.

editor.html
function init(count) {
  return { count, history: [] };
}
useReducer(reducer, initialCount, init);
localhost:3000

5Dispatch Has a Stable Identity

The dispatch function returned by useReducer never changes across re-renders — React guarantees the same reference every time. It's safe to omit from a useEffect or useCallback dependency array, and it can be passed down to deeply nested or memoized children without causing them to re-render just because a 'new' function reference showed up.

editor.html
const [state, dispatch] = useReducer(reducer, init);
// dispatch is the SAME function reference on every render
localhost:3000

Examples

Example 01Basic Usage
import { useReducer } from 'react';

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

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  return (
    <>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
    </>
  );
}
Example 02Advanced Example
function formReducer(state, action) {
  switch (action.type) {
    case 'field_changed':
      return { ...state, [action.field]: action.value };
    case 'reset':
      return { name: '', email: '' };
    default:
      return state;
  }
}

const [form, dispatch] = useReducer(formReducer, { name: '', email: '' });
dispatch({ type: 'field_changed', field: 'name', value: 'Ana' });
Example 03Lazy Initialization from Props
function init(initialCount) {
  return { count: initialCount, history: [] };
}

function reducer(state, action) {
  if (action.type === 'increment') {
    return { count: state.count + 1, history: [...state.history, state.count] };
  }
  return state;
}

function Counter({ startAt }) {
  const [state, dispatch] = useReducer(reducer, startAt, init);
  return <button onClick={() => dispatch({ type: 'increment' })}>{state.count}</button>;
}

Best Practices

  • Use useReducer instead of multiple related useState calls when several pieces of state tend to update together in response to the same events
  • Keep the reducer function pure, computing and returning a new state object based only on its state and action arguments, with no side effects performed inside it
  • Give dispatched action objects a clear, descriptive type field, and any needed extra data as additional properties, making the reducer's switch/if-else logic easy to follow
  • Pass an init function as useReducer's third argument to compute an expensive or prop-dependent initial state only once, on the first render
  • Take advantage of dispatch's stable identity — it's safe to omit from dependency arrays and to pass to memoized child components without extra memoization

Interview Question

Why might useReducer be a better choice than several separate useState calls for a form with multiple related fields?

Hint: Think about how updates to related fields might need to interact with each other, and where that interaction logic would need to live in each approach.

With several separate useState calls, one per field, each field's update logic is isolated and has no natural place to account for interactions between fields, like clearing a dependent field when another one changes, or validating one field's value against another's, forcing that cross-field logic to live awkwardly scattered across multiple event handlers instead. useReducer centralizes all of a form's state transition logic into a single reducer function that receives the complete current state and the specific action describing what just happened, giving it full context to implement cross-field interactions, validation, or resets cleanly in one place, and making that logic straightforward to test independently by simply calling the reducer function directly with sample state and action values, without needing to render any actual component at all.

Exercises

MediumPractice using useReducer in a real scenario.
View Solution
import { useReducer } from 'react';

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

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });
  return (
    <>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
    </>
  );
}
HardFix this reducer, which mutates state directly and therefore fails to trigger a re-render when an item is added.
View Solution
// Wrong
// function reducer(state, action) {
//   if (action.type === 'add') {
//     state.items.push(action.item);
//     return state;
//   }
// }

// Correct
function reducer(state, action) {
  if (action.type === 'add') {
    return { ...state, items: [...state.items, action.item] };
  }
  return state;
}

Frequently Asked Questions

Why might useReducer be a better choice than several separate useState calls for a form with multiple related fields?

With several separate useState calls, one per field, each field's update logic is isolated and has no natural place to account for interactions between fields, like clearing a dependent field when another one changes, or validating one field's value against another's, forcing that cross-field logic to live awkwardly scattered across multiple event handlers instead. useReducer centralizes all of a form's state transition logic into a single reducer function that receives the complete current state and the specific action describing what just happened, giving it full context to implement cross-field interactions, validation, or resets cleanly in one place, and making that logic straightforward to test independently by simply calling the reducer function directly with sample state and action values, without needing to render any actual component at all.

Why does a reducer branch that mutates state and returns the same object fail to trigger a re-render?

Exactly like useState, React decides whether to re-render by comparing the value returned from your update to the previous value using Object.is, which checks objects by reference. If a reducer branch mutates the existing state object in place — push()ing to an array field, or directly assigning a property — and then returns that same object, its reference never changed, so React's comparison sees no difference and skips the re-render, even though the object's contents did change. A pure reducer must construct and return a new object (typically with spread syntax) for every state transition, never mutate the argument it received.

Related Functions

usestateasynchronous-statecontext-api