How to Use the useReducer Hook

By Dave Ceddia Comment

Out of all the new React Hooks, and maybe just based on the name alone, this one seems poised to make the most 🔥 🔥 🔥

The word “reducer” evokes images of Redux for many – but you don’t have to understand Redux to read this post, or to use the new useReducer hook that comes with the React 16.7.0 alpha.

We’ll talk about what a “reducer” actually is, how you can take advantage of useReducer to manage complex state in your components, and what this new hook might mean for Redux. Will Redux get the hook? (I’m sorry, these puns just write themselves I can’t stop it)

In this post we’re looking at the useReducer hook. It’s great for managing more complicated state than you would want to manage with useState on its own.

Quick note: Hooks is currently in alpha and not yet ready for production use. The API could still change. I don't recommend rewriting any production apps with hooks at this stage. Leave comments at the Open RFC and check out the official docs and FAQ too.

Watch this video explaining what you can do with the useReducer hook (or just keep reading).

What’s a Reducer?

If you’re familiar with Redux or the reduce method on arrays, you know what a “reducer” is. If you aren’t familiar, a “reducer” is a fancy word for a function that takes 2 values and returns 1 value.

If you have an array of things, and you want to combine those things into a single value, the “functional programming” way to do that is to use Array’s reduce function. For instance, if you have an array of numbers and you want to get the sum, you can write a reducer function and pass it to reduce, like this:

let numbers = [1, 2, 3];
let sum = numbers.reduce((total, number) => {
  return total + number;
}, 0);

If you haven’t seen this before it might look a bit cryptic. What this does is call the function for each element of the array, passing in the previous total and the current element number. Whatever you return becomes the new total. The second argument to reduce (0 in this case) is the initial value for total. In this example, the function provided to reduce (a.k.a. the “reducer” function) will be called 3 times:

  • Called with (0, 1), returns 1.
  • Called with (1, 2), returns 3.
  • Called with (3, 3), returns 6.
  • reduce returns 6, which gets stored in sum.

Ok, but, what about useReducer?

I spent half a page explaining Array’s reduce function because, well, useReducer takes the same arguments, and basically works the same way. You pass a reducer function and an initial value (initial state). Your reducer receives the current state and an action, and returns the new state. We could write one that works just like the summation reducer:

useReducer((state, action) => {
  return state + action;
}, 0);

So… what triggers this? How does the action get in there? Good question.

useReducer returns an array of 2 elements, similar to the useState hook. The first is the current state, and the second is a dispatch function. Here’s how it looks in practice:

const [sum, dispatch] = useReducer((state, action) => {
  return state + action;
}, 0);

Notice how the “state” can be any value. It doesn’t have to be an object. It could be a number, or an array, or anything else.

Let’s look at a complete example of a component using this reducer to increment a number:

import React, { useReducer } from 'react';

function Counter() {
  // First render will create the state, and it will
  // persist through future renders
  const [sum, dispatch] = useReducer((state, action) => {
    return state + action;
  }, 0);

  return (
    <>
      {sum}

      <button onClick={() => dispatch(1)}>
        Add 1
      </button>
    </>
  );
}

Give it a try in this CodeSandbox.

You can see how clicking the button dispatches an action with a value of 1, which gets added to the current state, and then the component re-renders with the new (larger!) state.

I’m intentionally showing an example where the “action” doesn’t have the form { type: "INCREMENT_BY", value: 1 } or some other such thing, because the reducers you create don’t have to follow the typical patterns from Redux. The world of Hooks is a new world: it’s worth considering whether you find old patterns valuable and want to keep them, or whether you’d rather change things up.

A More Complex Example

Let’s look at an example that does more closely resemble a typical Redux reducer. We’ll create a component to manage a shopping list. We’ll see another hook here, too: useRef.

First we need to import the two hooks:

import React, { useReducer, useRef } from 'react';

Then create a component that sets up a ref and a reducer. The ref will hold a reference to a form input, so that we can extract its value. (We could also manage the input with state, passing the value and onChange props as usual, but this is a good chance to show off the useRef hook!)

function ShoppingList() {
  const inputRef = useRef();
  const [items, dispatch] = useReducer((state, action) => {
    switch (action.type) {
      // do something with the action
    }
  }, []);

  return (
    <>
      <form onSubmit={handleSubmit}>
        <input ref={inputRef} />
      </form>
      <ul>
        {items.map((item, index) => (
          <li key={item.id}>
            {item.name}
          </li>
        ))}
      </ul>
    </>
  );
}

Notice that our “state” in this case is an array. We’re initializing it to an empty array (the second argument to useReducer) and will be returning an array from the reducer function soon.

The useRef Hook

A quick aside, and then we’ll get back to the reducer, but I wanted to explain what useRef does.

The useRef hook allows you to create a persistent ref to a DOM node. Calling useRef creates an empty one (or you can initialize it to a value by passing an argument). The object it returns has a property current, so in the example above we can access the input’s DOM node with inputRef.current. If you’re familiar with React.createRef(), this works very much the same.

The object returned by useRef is more than just a way to hold a DOM reference, though. It can hold any value specific to this component instance, and it persists between renders. Sound familiar? It should!

useRef can be used to create generic instance variables, just like you can do with a React class component with this.whatever = value. The only thing is, writing to it counts as a “side effect” so you can’t change it during a render – only inside the body of a useEffect hook. (more on useEffect tomorrow!) The official Hooks FAQ has an example of using a ref as an instance variable.

Back to the useReducer example…

We’ve wrapped the input with a form so that pressing Enter will trigger the submit function. Now need to write the handleSubmit function that will add an item to the list, as well as handle the action in the reducer.

function ShoppingList() {
  const inputRef = useRef();
  const [items, dispatch] = useReducer((state, action) => {
    switch (action.type) {
      case 'add':
        return [
          ...state,
          {
            id: state.length,
            name: action.name
          }
        ];
      default:
        return state;
    }
  }, []);

  function handleSubmit(e) {
    e.preventDefault();
    dispatch({
      type: 'add',
      name: inputRef.current.value
    });
    inputRef.current.value = '';
  }

  return (
    // ... same ...
  );
}

We’ve filled out the reducer function with two cases: one for when the action has type === 'add', and the default case for everything else.

When the reducer gets the “add” action, it returns a new array that includes all the old elements, plus the new one at the end.

We’re using the length of the array as a sort of auto-incrementing (ish) ID. This works for our purposes here, but it’s not a great idea for a real app because it could lead to duplicate IDs, and bugs. (better to use a library like uuid or let the server generate a unique ID!)

The handleSubmit function is called when the user presses Enter in the input box, and so we need to call preventDefault to avoid a full page reload when that happens. Then it calls dispatch with an action. In this app, we’re deciding to give our actions a more Redux-y shape – an object with a type property and some associated data. We’re also clearing out the input.

Try the project at this stage in this CodeSandbox.

Remove an Item

Now let’s add the ability to remove an item from the list.

We’ll add a “delete” <button> next to the item, which will dispatch an action with type === "remove" and the index of the item to remove.

Then we just need to handle that action in the reducer, which we’ll do by filtering the array to remove the doomed item.

function ShoppingList() {
  const inputRef = useRef();
  const [items, dispatch] = useReducer((state, action) => {
    switch (action.type) {
      case 'add':
        // ... same as before ...
      case 'remove':
        // keep every item except the one we want to remove
        return state.filter((_, index) => index != action.index);
      default:
        return state;
    }
  }, []);

  function handleSubmit(e) { /*...*/ }

  return (
    <>
      <form onSubmit={handleSubmit}>
        <input ref={inputRef} />
      </form>
      <ul>
        {items.map((item, index) => (
          <li key={item.id}>
            {item.name}
            <button
              onClick={() => dispatch({ type: 'remove', index })}
            >
              X
            </button>
          </li>
        ))}
      </ul>
    </>
  );
}

Try it in CodeSandbox.

Exercise: Clear the List

We’ll add one more feature: a button that clears the list. This one is an exercise!

Insert a button above the <ul> and give it an onClick prop that dispatches an action with type “clear”. Then, add a case to the reducer that handles the “clear” action.

Open up the previous CodeSandbox checkpoint and make your changes (don’t worry, it won’t overwrite my sandbox).

Did you try it? Got it working? Nice job!

Did you scroll down hoping to just read the answer? I guarantee this Hooks stuff will seem a lot less magical once you try it out a bit on your own, so I urge you to give it a try!

So… is Redux Dead?

Many peoples’ first thought upon seeing the useReducer hook went something like… “well, React has reducers built in now, and it has Context to pass data around, so Redux is dead!” I wanted to give some thoughts on that here, because I bet you might be wondering.

I don’t think useReducer will kill Redux any more than Context killed Redux (it didn’t). I do think this further expands React’s capabilities in terms of state management, so the cases where you truly need Redux might be diminished.

Redux still does more than Context + useReducer combined – it has the Redux DevTools for great debugging, and middleware for customizability, and a whole ecosystem of helper libraries. You can pretty safely argue that Redux is used in plenty of places where it is overkill (including almost every example that teaches how to use it, mine included!), but I think it still has sticking power.

Redux provides a global store where you can keep app data centralized. useReducer is localized to a specific component. Nothing would stop you from building your own mini-Redux with useReducer and useContext, though! And if you want to do that, and it fits your needs, go for it! (plenty of people on Twitter already have, and posted screenshots) I’d still miss the DevTools personally, but I’m sure there’ll be 5 or 10 or 300 npm packages for that shortly, if there aren’t already.

tl;dr – Redux isn’t dead. Hooks don’t obsolete Redux. Time will tell, though. Hooks came out mere days ago and it’s still in alpha! I’m excited to see what new stuff the community builds with them.

Try It Out Yourself!

Here are a few tiny apps you can build to try out the useReducer hook on your own:

  • Make a “room” with a light that has 4 levels – off, low, medium, high – and change the level each time you press a button.
  • Make a “keypad” with 6 buttons that must be pressed in the correct order to unlock it. Each correct button press advances the state. Incorrect button presses reset it.
comments powered by Disqus