useReducer
Topic 5 in React Hooks: Tyler McGinnis
Remember the purpose of the ‘reduce’ or ‘fold’ pattern in functional programming.
We want to take a sequence of values (like an array) and return a single value. If we just have a loop operating on the individual values (like a forEach loop) we could only do this by using an external variable and mutating it during the iteration.
For example, think of summing a list of numbers. We’d have to have a placeholder value that lived outside the loop, and update it on each iteration. This introduces side-effects, not ideal.
So we have the functional pattern of ‘reduce’. Here we have a function, the reducer, that takes a value from the list, alongside a piece of state, applies an operation to that value and the state, and returns a new state. That new state is then used in the next iteration.
So if we want to sum a list, we start with some initial state (0), and a reducer that sums the state with the current value, passing that new state along.
We can apply this pattern to user interfaces too.
Say a user takes a series of actions, we can start with an initial state, apply the first action, return a new state. Apply the second action to that state, return a third state, and so on.
This pattern is so important that React includes a useReducer
hook to facilitate it.
There’s one big difference though. In an array context you could just pass an initial state and a reducer, like this, and you’d get a final state at the end:
[].reduce(reducer, initialState)
.
But in a UI context, we also need a way for user actions to invoke our reducer function, since this isn’t a batch process. So instead of just returning a final state, we return a state and a dispatch function to invoke the reducer function:
const [state, dispatch] = React.useReducer(reducer, initialState)
The dispatch function, when invoked, will in turn invoke the reducer function. Whatever you pass as an argument to dispatch
will be used as the second argument to the reducer
.
So reducer
would look like this:
function reducer(state, value) {
return state + value
}
Then you could, say call dispatch(1)
which, if the current state was 1
at the time would effectively call reducer(1,1)
and set the state as 2
.
A slightly closer to real-world scenario would be to pass, instead of a value to the reducer, an action definition, which the reducer would then handle. So for example something like this:
function reducer(state, action) {
if (action === 'increment') {
return state + 1;
} else if (action === 'decrement') {
return state - 1;
} else if (action === 'reset') {
return 0;
} else {
throw new Error('action not recognised');
}
}
We’ve now decoupled the logic of state update from the component - we’re modelling the state logic as state transitions, to which actions are mapped. The action, and how the state updates, are now separated.
Note that reducer is passed the current state as its first argument always. This makes it simple to update one piece of state based on another piece of state. Whenever updating state depends on the value of another piece of state, reach for useReducer over useState.
Eg in the lecture is where there is a ‘step’ value that influences how the count is incremented.
So fundamentally useState and useReducer both allow us to add state to function components. When to use which one?
First benefit of useReducer
is that it allows you to write more declarative state updates. Rather than having to write lots of imperative code that sets the state of each individual thing, you create a compound state object and then have your reducer declaratively describe the impact of different actions. You end up spreading the previous state and then just declaring the new values for affected properties, which is much easier.
The second benefit is that it’s easy to set the state value based on the prior state, especially if that is a different piece of prior state.
The third is that it helps minimize the dependency array for useEffect. Since the action is now decoupled from how the state should be updated, you can typically remove the state from the dependency array in useEffect. The effect now just dispatches the action type, not itself relying on any of the state values, since those values are encapsulated inside the reducer itself (which receives them direct from React).
Basically, if you have small pieces of state that update independently, using useState should be fine. But if they update together or depend on each other, use useReducer.