REACT

React useReducer Hook: Syntax, Usage, and Examples

The React useReducer hook helps you manage complex state logic in function components. It’s especially useful when state changes depend on previous values or involve multiple sub-values. This hook works similarly to a Redux reducer: you dispatch actions, and the reducer function determines how state updates.

You might choose useReducer React patterns over useState when dealing with more structured or scalable application state.


How React useReducer Works

The useReducer hook accepts two arguments: a reducer function and an initial state. It returns the current state and a dispatch function to trigger changes.

Syntax

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

The reducer is a pure function that receives the current state and an action, then returns a new state.

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

To update the state, you call the dispatch function with an action object:

dispatch({ type: 'increment' });

Example: useReducer for a Counter

Here's a simple counter example using the React usereducer hook:

import { 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();
  }
}

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

  return (
    <><p>Count: {state.count}</p>
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
    </>
  );
}

This approach separates the state logic from the UI, making the component easier to test and maintain.


You Can Manage More Complex State

useReducer shines when your state has multiple values or you need to update it based on actions.

const initialState = {
  user: null,
  loading: false,
  error: null
};

function reducer(state, action) {
  switch (action.type) {
    case 'FETCH_START':
      return { ...state, loading: true };
    case 'FETCH_SUCCESS':
      return { ...state, loading: false, user: action.payload };
    case 'FETCH_ERROR':
      return { ...state, loading: false, error: action.payload };
    default:
      return state;
  }
}

In this case, a single reducer handles all logic related to fetching a user.


You Should Use useReducer When State Logic Grows

React useState works well for simple use cases. But when you:

  • Depend on previous state for updates
  • Have multiple values in a single state object
  • Want better organization
  • Need a Redux-like pattern without Redux

…you should consider using useReducer.

This is often the case in form management, nested updates, undo/redo functionality, and state machines.


useReducer with Initial State from Props

Sometimes you want to derive initial state from props. React lets you use an initializer function as a third argument to useReducer.

const init = (initialCount) => ({ count: initialCount });

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

This pattern lets you defer expensive setup logic until the component actually mounts.


You Can Combine useReducer with Context

For global state sharing, combine useReducer React logic with React Context. This mimics Redux without extra libraries.

const StateContext = createContext();

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

  return (
    <StateContext.Provider value={{ state, dispatch }}>
      <ChildComponent />
    </StateContext.Provider>
  );
}

function ChildComponent() {
  const { state, dispatch } = useContext(StateContext);
  return <div>{state.value}</div>;
}

This pattern improves scalability and readability in large applications.


How to Use useReducer in React for Controlled Forms

Managing form state with useReducer lets you centralize logic and validations.

const formReducer = (state, action) => {
  return { ...state, [action.name]: action.value };
};

const [formState, dispatch] = useReducer(formReducer, {
  username: "",
  email: ""
});

<inputname="username"
  value={formState.username}
  onChange={(e) =>
    dispatch({ name: e.target.name, value: e.target.value })
  }
/>

This approach avoids managing multiple useState hooks for each field.


Debugging and Testing Reducers

Because reducer functions are pure, you can write tests for them without rendering components.

test('should increment count', () => {
  const initialState = { count: 0 };
  const action = { type: 'increment' };
  const result = reducer(initialState, action);
  expect(result.count).toBe(1);
});

This makes useReducer a great fit for apps that need strong test coverage.


Best Practices for useReducer in React

  • Keep the reducer pure: no side effects or async code inside it.
  • Use constants for action types to avoid typos.
  • Create action creators for readability.
  • Separate logic-heavy reducers into their own files.
  • Avoid unnecessary nesting of state.

Following these practices ensures that your useReducer logic is maintainable and scalable.


useReducer vs useState

Choosing between useReducer and useState depends on the complexity of the state you're managing.

  • Use useState when you're working with simple, independent values like toggles, counters, or form fields with straightforward logic. It's lightweight and easy to understand for these cases.
  • Switch to useReducer when state updates depend on previous values, when you have multiple related pieces of state, or when the logic for updating state gets repetitive or hard to manage with multiple useState hooks.
  • If your component needs to handle more structured logic, like conditionally updating nested properties or running multiple state updates at once, useReducer gives you a clearer and more maintainable way to do it.
  • In large applications or shared state scenarios, combining useReducer with Context can provide a Redux-like structure without the extra libraries.

By thinking about the shape and complexity of your state, you can choose the tool that fits best—useState for simplicity, or useReducer for structure and control.


Example: useReducer in a Todo App

const initialState = [];

function todoReducer(state, action) {
  switch (action.type) {
    case "ADD_TODO":
      return [...state, { id: Date.now(), text: action.payload }];
    case "REMOVE_TODO":
      return state.filter(todo => todo.id !== action.payload);
    default:
      return state;
  }
}

Use dispatch to add or remove todos:

dispatch({ type: "ADD_TODO", payload: "Buy milk" });
dispatch({ type: "REMOVE_TODO", payload: 12345 });

This structure handles dynamic lists in a clean, scalable way.


The useReducer React hook offers a structured way to manage state updates, especially when those updates rely on previous values or involve multiple conditions. You define a reducer function, pass an initial state, and use dispatch to trigger changes.

By understanding how to use useReducer in React effectively, you can:

  • Simplify complex logic
  • Keep state transitions predictable
  • Centralize updates into one function
  • Improve testability and organization

This hook is a solid choice for advanced state management in React apps without needing a full external state library.

Learn to Code in React for Free
Start learning now
button icon
To advance beyond this tutorial and learn React by doing, try the interactive experience of Mimo. Whether you're starting from scratch or brushing up your coding skills, Mimo helps you take your coding journey above and beyond.

Sign up or download Mimo from the App Store or Google Play to enhance your programming skills and prepare for a career in tech.

You can code, too.

© 2025 Mimo GmbH

Reach your coding goals faster