Examples of the useReducer Hook

By Dave Ceddia updated

The word “reducer” might evoke images of Redux – but I promise you don’t have to understand Redux to read this post, or to use the new useReducer hook that comes with the React 16.8.

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 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.

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

Video: The useReducer Hook in Action

The useReducer Hook

What’s a Reducer?

A “reducer” is a fancy word for a function that takes 2 values and returns 1 value.

(if you’ve used Redux or the reduce method on arrays, you probably alread know what a “reducer” is!)

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.

What does useReducer do?

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 function to run? How does the action get in there? Good questions.

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);

We’re using ES6 destructuring syntax to pull out the 2 values from the array and name them sum and dispatch here.

Important to notice, the “state” can be any kind of 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 useReducer example of a component using it 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>
    </>
  );
}

Our state is an array here, and we’re initializing it to an empty array (the second argument to useReducer is the initial value). Soon we’ll fill in the reducer function to return a new array.

What The useRef Hook Does

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, or really to any value. React will persist this value between re-renders (between calls to your component function).

We need this because we can’t just store things in local variables – they’d go out of scope as soon as the component function returns (React components are really just functions, remember).

Calling useRef creates an empty ref object by default, or you can initialize it to some other value by passing an argument.

The object it returns will always have a current property, and we access the value inside the ref with inputRef.current. Don’t forget the .current!

If you’re familiar with React.createRef(), this works very much the same. The gist is: by passing a ref object in as the ref prop on a DOM element, React will automatically set that ref object’s current to refer to the DOM element after it is rendered. Then, later, you can access the DOM node through theRefVariable.current.

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.

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 – you should only change a ref inside the body of a useEffect hook. 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 (because that’s how HTML forms work by default). 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 we need to call preventDefault to avoid a full page reload. Then it calls dispatch with an action. We’re also clearing out the input.

In this app, our actions are objects with a type property and some associated data, but they can be anything you want (plain strings, numbers, more complex objects, etc).

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 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

Let’s 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 to just read the answer? I guarantee this Hooks stuff will make more sense if you try it out 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 are fewer.

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.

One big difference is 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! Making it scalable and performant is harder, though. If your needs are more complex than a few bits of data that change infrequently, you might want to look at Redux with the Redux Toolkit or MobX or Recoil or some other state management solution.

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.