React useReducer Hook: The Ultimate Guide

State management means keeping track of how our data changes over time. In React, we can manage state with hooks or using an external state management library like Redux. In this article, we will explore a hook called useReducer and learn about its capabilities for state management.

Introduction to React useReducer

useReducer is a React Hook that gives us more control over state management than useState, making it easier to manage complex states. Its basic structure is:

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

When combined with other React Hooks such as useContext, it works almost similarly to Redux. The difference is that Redux creates a global state container (a store), while useReducer creates an independent state container within our component.

useReducer can be used to manage state that depends on previous states and efficiently handle multiple, complex states.

Prerequisites

To understand this article, the following are required:

  • Good knowledge of JavaScript and React, with emphasis on functional programming.

  • Experience with some React Hooks such as useState is not strictly required but preferred.

How Does useReducer Work?

To understand useReducer, let’s first look at JavaScript’s Array.prototype.reduce() method.

Given an array, the reduce() method executes a reducer callback function on each element in the array and returns a single final value.

The reduce() method takes in two parameters: a reducer function (required) and an initial value (optional). Take a look at this example below:

const numbers = [2, 3, 5, 7, 8]; // an array of numbers
const reducer = (prev, curr) => prev + curr; // reducer callback function
const initialValue = 5; // initial value

const sumOfNumbers = numbers.reduce(reducer, initialValue); // reduce() method
console.log(sumOfNumbers); // prints 30, a sum of all the elements in the numbers array and the initial value

On its first iteration, prev takes in the initialValue, curr takes the current element in the array (the first element in this case), and the function executes with both values. The result is then stored as the new prev, and curr becomes the next element in the array.

If there is no initial value, prev starts with the first element of the array.

React’s useReducer works in a similar way:

  • It accepts a reducer function and an initialState as parameters.

  • Its reducer function accepts a state and an action as parameters.

  • The useReducer returns an array containing the current state returned by the reducer function and a dispatch function for passing values to the action parameter.

The Reducer Function

The reducer function is a pure function that accepts state and action as parameters and returns an updated state. Here’s its structure:

const reducer = (state, action) => {
  // logic to update state with value from action
  return updatedState;
};

The action parameter helps us define how to change our state. It can be a single value or an object with a label (type) and some data to update the state (payload). It gets its data from useReducer's dispatch function.

We use conditionals (typically a switch statement) to determine what code to execute based on the type of action (action.type).

Understanding useReducer with Examples

Example 1: A Simple Counter

import React, { useReducer } from 'react';

const Counter = () => {
  const initialState = 0;
  const reducer = (state, action) => state + action;

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

  return (
    <div>
      <h3>Counter</h3>
      <h1>{state}</h1>
      <button onClick={() => dispatch(1)}>Increase</button>
    </div>
  );
};

export default Counter;

Explanation:

  • The initialState is 0, so when the component is initially displayed, <h1>{state}</h1> shows 0.

  • When a user clicks the button, it triggers the dispatch, which sets the value of action to 1 and runs the reducer function.

  • The reducer function returns the sum of the current state and the action.

  • The result is passed as the new, updated state and displayed on the browser.

Example 2: Counter with Extra Steps

import React, { useReducer } from 'react';

const Counter = () => {
  const initialState = 0;
  const reducer = (state, action) => {
    switch (action.type) {
      case 'add':
        return state + action.payload;
      case 'subtract':
        return state - action.payload;
      case 'reset':
        return initialState;
      default:
        throw new Error();
    }
  };

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

  return (
    <div>
      <h3>Counter</h3>
      <h1>{state}</h1>
      <button onClick={() => dispatch({ type: 'subtract', payload: 1 })}>
        Decrease
      </button>
      <button onClick={() => dispatch({ type: 'reset' })}>Reset</button>
      <button onClick={() => dispatch({ type: 'add', payload: 2 })}>
        Increase
      </button>
    </div>
  );
};

export default Counter;

Explanation:

  • The dispatch function now passes an object with type and payload.

  • The reducer function uses a switch statement to determine how to update the state based on the action.type.

Example 3: Smart Home Controls

import React, { useReducer } from 'react';

const appliances = [
  { name: 'bulbs', active: false },
  { name: 'air conditioner', active: true },
  { name: 'music', active: true },
  { name: 'television', active: false },
];

const reducer = (state, action) => {
  switch (action.type) {
    case 'deactivate':
      return state.map((appliance) =>
        appliance.name === action.payload
          ? { ...appliance, active: false }
          : appliance
      );
    case 'activate':
      return state.map((appliance) =>
        appliance.name === action.payload
          ? { ...appliance, active: true }
          : appliance
      );
    default:
      return state;
  }
};

const SmartHome = () => {
  const [state, dispatch] = useReducer(reducer, appliances);

  return (
    <div className="container">
      <h1>SmartHome</h1>
      <div className="grid">
        {state.map((appliance, idx) => (
          <div key={idx} className="card">
            <h2>{appliance.name}</h2>
            {appliance.active ? (
              <button
                className="status active"
                onClick={() =>
                  dispatch({ type: 'deactivate', payload: appliance.name })
                }
              >
                Active
              </button>
            ) : (
              <button
                className="status inactive"
                onClick={() =>
                  dispatch({ type: 'activate', payload: appliance.name })
                }
              >
                Not active
              </button>
            )}
          </div>
        ))}
      </div>
    </div>
  );
};

export default SmartHome;

Explanation:

  • The reducer function toggles the active status of appliances.

  • The dispatch function updates the state based on the action.type.

Example 4: Shopping Cart

import React, { useReducer } from 'react';

const initialState = {
  input: '',
  items: [],
};

const reducer = (state, action) => {
  switch (action.type) {
    case 'add':
      return {
        ...state,
        items: [...state.items, action.payload],
        input: '',
      };
    case 'input':
      return { ...state, input: action.payload };
    case 'delete':
      return {
        ...state,
        items: state.items.filter((item) => item.id !== action.payload),
      };
    default:
      return state;
  }
};

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

  const handleChange = (e) => {
    dispatch({ type: 'input', payload: e.target.value });
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    dispatch({
      type: 'add',
      payload: {
        id: new Date().getTime(),
        name: state.input,
      },
    });
  };

  return (
    <div>
      <h1>Shopping Cart</h1>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={state.input}
          onChange={handleChange}
        />
      </form>
      <div>
        {state.items.map((item, index) => (
          <div key={item.id}>
            {index + 1}. {item.name}
            <button
              onClick={() =>
                dispatch({ type: 'delete', payload: item.id })
              }
            >
              x
            </button>
          </div>
        ))}
      </div>
    </div>
  );
};

export default ShoppingCart;

Explanation:

  • The reducer function handles adding, updating, and deleting items in the cart.

  • The dispatch function updates the state based on user input.

useState vs useReducer

FeatureuseStateuseReducer
State TypeSimple valuesComplex objects/arrays
UpdatesDirectAction-based
Logic LocationIn componentCentralized in reducer
PerformanceGood for shallowBetter for deep updates

When to Use useReducer Hook

  1. State depends on previous values: For example, counters or toggles.

  2. Managing complex states: Such as nested objects or arrays.

  3. Updating state based on another state: For example, adding items to a cart based on user input.

When Not to Use the useReducer Hook

  1. Simple state: Use useState for basic state needs.

  2. Global state management: Use libraries like Redux or MobX for large applications.

Conclusion

This article demonstrated how to handle complex states with useReducer, exploring its use cases and tradeoffs. It’s important to note that no single React hook can solve all our challenges, and knowing what each hook does can help us decide when to use it.

The examples used in this article are available on CodeSandbox.