George Song

React.useReducer Reducer Patterns, Part 2

In part 1, we looked at reducer patterns that didn’t use any parameters or only used 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 exploring other reducer patterns.

Table of Contents

Reducer Using action

Remember that (currentState, action) => newState is the complete reducer signature. In the 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? It receives the action parameter via the dispatch function returned by the hook: [state, dispatch] = useReducer(reducer). The dispatch function takes a single optional parameter: dispatch(action).

Use Both currentState and action Parameters

Let’s look at an example where we use both parameters 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 (the value from <input name="numberToAdd">). The reducer takes the current state (count) and the action (valueToAdd) to produce a new state (count + valueToAdd).

Many examples of actions you’ve seen use the shape { type, payload }. While this is a common 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 parameter.

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 users to change each 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 as Actions

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

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.