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
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 thedispatch()
andreducer()
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.