React useReducer

Summary: in this tutorial, you will learn how to use the React useReducer hook to handle complex state logic.

Introduction to the React useReducer() hook

The useReducer hook is an alternative to the useState hook, allowing you to manage the state of components:

  • useReducer hook produces the state.
  • Changing the state triggers a component re-render.

The useReducer hook is useful when:

  • The component has multiple closely related pieces of state.
  • The future state value depends on the current state.

To use the useReducer hook, you follow these steps:

Step 1. Import the useReducer hook from the react library:

import { useReducer } from 'react';Code language: JavaScript (javascript)

Step 2. Define a reducer() function that takes two parameters state and action:

function reducer(state, action) {
  // ...
}Code language: JavaScript (javascript)

Step 3. Use the useReducer hook in the component:

function MyComponent() {
   const [state, dispatch] = useReducer(reducer, initialArg, init?);
   // ...
}Code language: JavaScript (javascript)

Here’s the syntax of the useReducer hook:

  • reducer is a function that determines how the state gets updated.
  • initialArg is the initial state.
  • init is an optional parameter that specifies a function returning the initial state. If you omit it, the initial state is set to initialArg. Otherwise, the initial state is set to the result of calling init(initialArg).

The useReducer() function returns an array that has exactly two values:

  • state is the current state.
  • dispatch function lets you update the state to the new one and trigger a re-render.

The following shows how the useReducer hook works.

useReducer

React useReducer() hook example

The following illustrates a counter app:

If you click the increment button, it’ll increment the count by one. If you click the decrement button, it’ll decrement the count by one.

If you change the step and click the increment or decrement buttons, it’ll increment or decrement the count by the step.

Download the counter app that uses the useState hook,

Here’s the source code of the App component:

import { useState } from 'react';
import './App.css';

const App = () => {
  const [count, setCount] = useState(0);
  const [step, setStep] = useState(1);

  const increment = () => setCount(count + step);
  const decrement = () => setCount(count - step);
  const handleChange = (e) => setStep(parseInt(e.target.value));

  return (
    <>
      <header>
        <h1>Counter</h1>
      </header>
      <main>
        <section className="counter">
          <p className="leading">{count}</p>
          <div className="actions">
            <button
              type="button"
              className="btn btn-circle"
              onClick={decrement}
            >
              -
            </button>
            <button
              type="button"
              className="btn btn-circle"
              onClick={increment}
            >
              +
            </button>
          </div>
        </section>

        <section className="counter-step">
          <label htmlFor="step">Step</label>
          <input
            id="step"
            type="range"
            min="1"
            max="10"
            value={step}
            onChange={handleChange}
          />
          <label>{step}</label>
        </section>
      </main>
    </>
  );
};

export default App;Code language: JavaScript (javascript)

The App component has two related pieces of state:

  • count
  • step

Also, the next state depends on the previous one:

const increment = () => setCount(count + step);
const decrement = () => setCount(count - step);Code language: JavaScript (javascript)

We’ll replace the useState hook with the useReducer hook.

Step 1. Define a reducer() function that has two parameters state and action:

const reducer = (state, action) => {
  // ...
};Code language: JavaScript (javascript)

Step 2. Remove the useState hooks and use the useReducer hook instead:

// const [count, setCount] = useState(0);
// const [step, setStep] = useState(1);

const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 });Code language: JavaScript (javascript)

On the left side:

  • The state is an object that includes all state variables including count and step.
  • The dispatch is functionally equivalent to the setCount and setStep functions.

On the right side:

  • reducer is a function that decides how to update the count and step properties in the state object.
  • {count: 0, step: 1} is the initial state.

The following diagram illustrates the equivalence of useState and useReducer hooks:

React useReducer

If you want to update the state, you can call the dispatch() function, which is a function that you get back from calling the useReducer() function.

When you call the dispatch() function, React will find the reducer function and execute it.

The reducer function should return a new state. If it returns nothing, the state will be undefined.

The reducer function must not directly modify the state. Instead, it should return a new copy of the state with the updated properties.

Additionally, the reducer() function cannot contain async/await, requests to API, promises, or changing outside variables. In other words, it must be a pure function.

When calling the dispatch() function, you need to pass an object to tell the reducer() function how to update the state.

By convention, you need to pass an action object with two properties:

{type: 'ACTION', payload: value }Code language: JavaScript (javascript)
  • type is a string that tells the reducer which state to update.
  • payload is a value passed to the reducer.

In the action object, the type property is required whereas the payload property is optional.

For example, if the user clicks the increment button, you need to call the dispatch() function as follows:

dispatch({ type: 'INCREMENT' })Code language: JavaScript (javascript)

In the reducer() function, you can check the action object and update the state accordingly:

if(action.type === 'INCREMENT') {
    return {
        ...state,
        count: state.count + state.step
    };
}Code language: JavaScript (javascript)

Similarly, if the user clicks the decrement button, you need to call the dispatch() function with a different action type:

dispatch({ type: 'DECREMENT' })Code language: JavaScript (javascript)

And check the action object inside the reducer() function to reduce the count property of a state:

if(action.type === 'DECREMENT') {
    return {
        ...state,
        count: state.count - state.step
    };
}Code language: JavaScript (javascript)

If the user changes the state, you can call the dispatch() function like this:

dispatch({ type: 'CHANGE_STEP', payload: newStep });Code language: JavaScript (javascript)

In the reducer() function, you can update the step property of the state accordingly:

if(action.type === 'CHANGE_STEP') {
    return {
        ...state,
        step: action.payload
    };
}Code language: JavaScript (javascript)

Since the reducer() function deals with many action types, we can use a switch…case statement:

const reducer = (state, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return {
        ...state,
        count: state.count + state.step,
      };
    case 'DECREMENT':
      return {
        ...state,
        count: state.count - state.step,
      };
    case 'CHANGE_STEP':
      return {
        ...state,
        count: action.payload,
      };
    default:
      throw new Error(`action type ${action.type} is unexpected.`);
  }
};Code language: JavaScript (javascript)

Note that if the action type is not determined, you can throw an error or just ignore it.

The following shows the complete App component that uses the useReducer hook instead of useState hook:

import { useReducer, useState } from 'react';
import './App.css';

const reducer = (state, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return {
        ...state,
        count: state.count + state.step,
      };
    case 'DECREMENT':
      return {
        ...state,
        count: state.count - state.step,
      };
    case 'CHANGE_STEP':
      return {
        ...state,
        step: action.payload,
      };
    default:
      throw new Error(`action type ${action.type} is unexpected.`);
  }
};

const App = () => {
  // const [count, setCount] = useState(0);
  // const [step, setStep] = useState(1);

  const [state, dispatch] = useReducer(reducer, { count: 0, step: 1 });

  // const increment = () => setState(count + step);
  // const decrement = () => setCount(count - step);
  // const handleChange = (e) => setStep(parseInt(e.target.value));

  const increment = () => dispatch({ type: 'INCREMENT' });
  const decrement = () => dispatch({ type: 'DECREMENT' });
  const handleChange = (e) =>
    dispatch({
      type: 'CHANGE_STEP',
      payload: parseInt(e.target.value),
    });

  return (
    <>
      <header>
        <h1>Counter</h1>
      </header>
      <main>
        <section className="counter">
          <p className="leading">{state.count}</p>
          <div className="actions">
            <button
              type="button"
              className="btn btn-circle"
              onClick={decrement}
            >
              -
            </button>
            <button
              type="button"
              className="btn btn-circle"
              onClick={increment}
            >
              +
            </button>
          </div>
        </section>

        <section className="counter-step">
          <label htmlFor="step">Step</label>
          <input
            id="step"
            type="range"
            min="1"
            max="10"
            value={state.step}
            onChange={handleChange}
          />
          <label>{state.step}</label>
        </section>
      </main>
    </>
  );
};

export default App;Code language: JavaScript (javascript)

Download Project source code

Download the project source code

Using Immer package to work with immutable state

In the reducer function, we cannot mutate the state directly but return a copy of the modified state. This is quite inconvenient.

To work with the state more conveniently, you can use the Immer package. So instead of returning a copy of the modified state like this:

return {
  ...state,
  count: state.count + state.step,
};
Code language: JavaScript (javascript)

You can modify the state directly as follows:

state.count = state.count + state.step;Code language: JavaScript (javascript)

You can follow these steps to use the Immer package:

Step 1. Install the Immer package:

npm install immerCode language: JavaScript (javascript)

Step 2. Import the produce function into the component:

import { produce } from 'immer';Code language: JavaScript (javascript)

Step 3. Third, wrap the reducer function with the produce function:

const [state, dispatch] = useReducer(produce(reducer), { count: 0, step: 1 });Code language: JavaScript (javascript)

Step 4. Simplify the reducer() function that directly modifies the state:

const reducer = (state, action) => {
  switch (action.type) {
    case 'INCREMENT':
      state.count = state.count + state.step;
      break;
    case 'DECREMENT':
      state.count = state.count - state.step;
      break;
    case 'CHANGE_STEP':
      state.step = action.payload;
      break;
    default:
      throw new Error(`action type ${action.type} is unexpected.`);
  }
};Code language: JavaScript (javascript)

Download the project source code

Summary

  • The useReducer hook is an alternative to the useState hook.
  • Use the useReducer hook when the component has multiple closely related pieces of state and the future state depends on the current state.
  • Use the Immer package to work with the state more conveniently.
Was this tutorial helpful ?