How to Use React's useReducer Hook

How to Use React's useReducer Hook


About the author

Telmo Goncalves is a software engineer with over 13 years of software development experience and an expert in React. He’s currently Engineering Team Lead at Marley Spoon.

Check out more of his work on telmo.im


Back in 2018, Facebook and React introduced React Hooks during their React Conf 2018 with the idea that they could be used to mimic lifecycle methods and state management in functional components.

Since their release, React Hooks have been quickly adopted by the React development community; Hook functions like useState and useEffect are especially popular.

Another Hook which I’ve found to be very useful and relatively underutilized is useReducer. Using it has some distinct advantages React developers should keep in mind.

In this article, I’ll discuss when to use useReducer alongside a simple example illustrating how it’s especially useful. If you already know your way around Redux, you’re already familiar with how it works.

Note that this article assumes the reader has a general understanding of React.

When to useReducer

Of the Hooks that were introduced by React, two serve as the primary functions for handling state objects and transitions: useState and useReducer.

When dealing with handling or manipulating primitive elements of the state, most developers typically resort to useState. However, while useState is very efficient for simple state handling in a functional component, it falls short when we need to deal with more complex objects and operations.

Like useState, useReducer is used to manage local state in functional components, but it also allows us to deal with more complex state manipulation by accepting a reducer and an initial state and then returning the updated actual state with the dispatch function.

In short, there’s an advantage to using useReducer when dealing with something in the functional component state which is more complex than changing primitive values - or - if you want sustain a more predictable and maintainable state.

And then there is useContext, but for the sake of simplicity, I won’t go into detail about it here.

The Component

For the sake of example, let’s assume we are building an online store. We’ll start with a simple component where we have one single product.

import React from "react";
export default function MainStore() {
  return (
    <div className="product">
      Macbook Pro 13"
      <button>Add to Cart</button>
    </div>
  );
}

Now let’s see how we can use useReducer. First we set our component initial state. Say we have a quantity of 0:

const initialState = { quantity: 0 };

Now we need to build a reducer. In this function we are specifying what actions will be performed in it:

function reducer(state, action) {
  switch (action.type) {
    case "ADD_TO_CART":
      return { quantity: state.quantity + 1 };
    case "REMOVE_FROM_CART":
      return { quantity: state.quantity - 1 };
    default:
      throw new Error();
  }
}

So, we can add actions like ADD_TO_CART and REMOVE_FROM_CART.

Pretty simple math here, in case we’re adding to our cart we’ll grab our current state quantity state.quantity and add 1, otherwise, we’ll remove 1 from the quantity.

useReducer

First, import useReducer from React:

import React, { useReducer } from "react";

Inside our component, let’s initialize our reducer. Notice that we are using both our reducer function and the initialState set previously:

export default function MainStore() {
  const [state, dispatch] = useReducer(reducer, initialState);
  
  // [ ... ]

Great! Let’s do a little debugging to verify expected behavior. Before we actually start using our reducer, let’s see what’s inside our state.

Inside the return of the component, we put:

<pre>
  <code>
    {JSON.stringify(state)}
  </code>
</pre>

The output in the browser should be {"quantity":0}, the same as our initialState.

So, we now know that state stores the state of our reducer, let’s take a look at the dispatch method. As the name suggests, it’ll be used to dispatch our actions, both ADD_TO_CART and REMOVE_FROM_CART.

Without removing our debugger (JSON.stringify), let’s create a button that will dispatch an action to add the quantity by 1:

<button onClick={() => dispatch({ type: "ADD_TO_CART" })}>
  Add!
</button>

Amazing! We’re dispatching an action with type ADD_TO_CART, when this button is clicked it’ll run this piece of code from our reducer function:

return { quantity: state.quantity + 1 };

Every time the button is clicked, the debugger will increase the quantity value by 1.

Similarly, to remove an item from the cart we just need to change the type of the dispatch:

<button onClick={() => dispatch({ type: "REMOVE_FROM_CART" })}>
  Remove!
</button>

Conclusion

If you are tired of refactoring functional components into class components due to an inability to effectively manage a complicated state, looking for something that had more functionality than useState, or just didn’t understand the advantages of useReducer, this should be a great starting point to get familiar with it and its application.

Knowing when to use useReducer over useState is not always a clear. Remember that leveraging useReducer may be the best choice when:

  • Dealing with objects or components in state.
  • Handling more complex transactions/transitions in state.
  • Dealing with state properties that are tied to one another.
  • Working inside of larger applications or complex components.

Telmo regularly posts helpful React development tips and guides on Twitter. Be sure to follow him at @telmo


About PullRequest

PullRequest is a platform for code review, built for teams of all sizes. We have the world's largest network of on-demand reviewers, backed by best-in-class automation tools. Because code quality is important.

Learn more about PullRequest

Telmo Goncalves headshot
by Telmo Goncalves

September 24, 2020