George Song

React.useReducer Reducer Patterns, Part 2

July 26, 2020 (updated April 2, 2021)

In part 1, we looked at reducer patterns which didn’t rely on any params, or just the current state to calculate the next state. We also learned that the primary job of a reducer is to produce a new state.

Let’s continue to explore other reducer patterns.

Table of Contents

Reducer Using action

Sequence diagram of React.useReducer dispatch flow

Remember that (currentState, action) => newState is the complete reducer signature. In patterns we’ve explored so far, we haven’t used the action parameter. We know the hook automatically supplies the current state to the reducer, but how does a reducer receive its action parameter? Via the dispatch function we get when we create the hook: [state, dispatch] = useReducer(reducer). The dispatch function takes a single optional param: dispatch(action).

Use Both currentState and action Params

Let’s look at an example where we use both params with the reducer function.

const reducer = (count, valueToAdd) => count + valueToAdd;const [count, addToCount] = React.useReducer(reducer, 0);
return (
  <main>
    <div>Count: {count}</div>    <form
      onSubmit={(e) => {
        e.preventDefault();
        const valueToAdd = Number(e.currentTarget.numberToAdd.value);
        addToCount(valueToAdd);      }}
    >
      <label>
        Add to count:{" "}
        <input
          name="numberToAdd"
          type="number"
          defaultValue={1}
          style={{ width: "4em" }}
        />
      </label>
      <button>Add</button>
    </form>
  </main>
);

What’s happening? When we submit the form, we dispatch (addToCount) an action (<input name="numberToAdd">’s value). The reducer takes the current state (count) and the action (valueToAdd) to produce a new state (count + valueToAdd).

Many examples you’ve seen of actions take the shape of { type, payload }. While that’s the convention, an action can be anything you like (including undefined). In this example, an action is simply a number.

👩‍💻 Try It Out

Use action Param Only

Just like how we can choose to only use the currentState param in a reducer, we can choose to only use the action param.

const reducer = (_, newCount) => newCount;const [count, setCount] = React.useReducer(reducer, 0);
return (
  <main>
    <div>Count: {count}</div>
    <form
      onSubmit={(e) => {
        e.preventDefault();
        const valueToAdd = Number(e.currentTarget.numberToAdd.value);
        setCount(count + valueToAdd);      }}
    >
      ...
    </form>
  </main>
);

By switching up a few lines, our reducer now only relies on the action (newCount) to calculate its new state.

🤔 Wait a minute, that looks a lot like React.useState.

👩‍💻 Try It Out

Implement a Simple useState

Replace

const reducer = (_, newCount) => newCount;
const [count, setCount] = React.useReducer(reducer, 0);

with

const reducer = (_, newState) => newState;
const useState = (initialState) => React.useReducer(reducer, initialState);
const [count, setCount] = useState(0);

We have ourselves a simple useState! You can see that React.useState is syntactic sugar for React.useReducer, simplifying the common use case of updating a single state value. A complete re-implementation of useState is a few more lines of code. See Kent C. Dodd’s “How to implement useState with useReducer” if you’re interested in an in-depth explanation.

👩‍💻 Try It Out

Implement a State Updater

Use Case

We have a user profile form that allows them to change each entry value in the state object.

Solution

One elegant solution is to use the object spread syntax to create the next state.

const reducer = (info, updates) => ({ ...info, ...updates });const initialValue = {
  name: "Pat Doe",
  twitter: "@pdough",
  email: "pat@pdough.me",
  website: "https://pdough.me",
};
const [info, update] = React.useReducer(reducer, initialValue);
return (
  <main>
    <pre>{JSON.stringify(info, null, 2)}</pre>    <form>
      {Object.entries(info).map(([key, value]) => (        <label key={key} style={{ display: "block" }}>
          {key}:{" "}
          <input
            value={value}
            onChange={(e) => update({ [key]: e.currentTarget.value })}          />
        </label>
      ))}
    </form>
  </main>
);

👩‍💻 Try It Out

Dispatch Functions

Wait, say what? 🤔

const reducer = (count, action) => action(count);const [count, dispatch] = React.useReducer(reducer, 0);const add = (value) => dispatch((count) => count + value);const subtract = (value) => dispatch((count) => count - value);
return (
  <main>
    <div>Count: {count}</div>    <form>
      <label>
        Change count by:{" "}
        <input
          name="modifier"
          type="number"
          defaultValue={1}
          style={{ width: "4em" }}
        />
      </label>
      <button
        type="button"
        onClick={(e) => add(Number(e.currentTarget.form.modifier.value))}      >
        Add
      </button>
      <button
        type="button"
        onClick={(e) => subtract(Number(e.currentTarget.form.modifier.value))}      >
        Subtract
      </button>
    </form>
  </main>
);

Yup, as stated earlier, an action can be anything you like. In this example, we’re dispatching callback functions! 🤯

👩‍💻 Try It Out

Takeaways

  • Dispatched actions can take any shape you want: undefined, a value, an object, or even a function.
  • The action parameter is what’s commonly shared between the dispatch() and reducer() functions—it’s their private, internal contract.
  • React.useState is syntactic sugar for one specific use case of React.useReducer.

Intermission

You can implement a reducer in any way you like, as long as it fulfills the contract of ([currentState], [action]) => newState. You have complete freedom in deciding what an action looks like, and how you want to calculate the new state.

We’ve explored different ways of writing the reducer function, and we haven’t even come across the familiar switch statement yet. Don’t worry, we’ll get to that in part 3.