Discover the benefits and implementation of the useReducer React hook!

Ever since the introduction of hooks in React, I have found them to be incredibly useful in several projects. They offer a more streamlined approach to writing code, making it feel like “real” JavaScript. By eliminating the need for class-based components, functional components can now handle state management effectively.

For those who are new to hooks, I recommend checking out my introductory guide to React hooks here.

One specific hook that I frequently use is useReducer.

import React, { useReducer } from 'react'

The useReducer hook serves as a state management tool, similar to useState, but with more complexity. The key distinction between useState and useReducer lies in how state is altered. When using useReducer, state changes are determined by passing messages rather than directly calling an updater function.

If you’re familiar with Redux, the concept is quite similar. A reducer is a pure function that calculates the next state based on the previous state and the dispatched action.

(currentState, action) => newState

But what exactly does “pure function” mean in this context? Essentially, a pure function takes an input and produces an output without modifying the input or any external factors. In the case of reducers, they return an entirely new state that replaces the previous one.

To maintain the integrity of the reducer, remember the following guidelines:

  • Avoid modifying the reducer’s arguments
  • Refrain from generating side effects (e.g., making API calls that change external data)
  • Avoid calling non-pure functions that produce different outputs based on factors other than their input (e.g., Date.now() or Math.random())

While there are no hard constraints, adhering to these rules simplifies the testing process, as reducers remain free of any side effects. Centralizing state management through reducers allows components to modify state by sending messages and enables the utilization of more complex state structures within components.

To illustrate how useReducer works, let’s consider an example involving a counter component.

In this case, the useReducer hook requires a reducer function as its first argument and an initial state value, which is an integer starting from 0.

const Counter = () => {
  const [state, dispatch] = useReducer(reducer, 0)
}

The reducer function, as previously mentioned, accepts the current state and an action (could be any value of your choosing). In this example, the action is a string.

const reducer = (state, action) => {
  switch (action) {
    case 'INCREMENT':
      return state + 1
    case 'DECREMENT':
      return state - 1
    default:
      throw new Error()
  }
}

To create a functioning app, we also need to incorporate JSX code within the component.

const Counter = () => {
  const [count, dispatch] = useReducer(reducer, 0)
  return (
    <>
      Counter: {count}
      <button onClick={() => dispatch('INCREMENT')}>+</button>
      <button onClick={() => dispatch('DECREMENT')}>-</button>
    </>
  )
}

Feel free to explore the full example on CodePen for a hands-on experience.

Now, imagine a scenario where the state can be an object with numerous properties, and different actions solely modify individual properties. In such cases, the useReducer hook proves highly advantageous.