useReducer
The useReducer
hook is similar to useState
, but gives us a more structured approach for updating complex values.
We typically use useReducer
when our state has multiple sub-values, e.g. an object containing keys that we want to update independently.
API
The useReducer
hook requires 2 arguments, and has an optional 3rd argument:
reducer
- a pure function that takes a state and an action, and returns a new state value based on the actioninitialState
- any initial state value, just likeuseState
initializer
(optional) - this is uncommon, but we'll briefly introduce it later.
The useReducer
hook returns the current state, and a dispatch
function to update the state.
Dispatch
The dispatch function takes an action
, and calls the reducer to update the state.
In the example below, the initial state is {color: 'black', pet: 'cat'}
. When we select dog
from the dropdown, dispatch is called with {type: types.PET, value: 'dog'}
as its argument. This argument gets passed to the reducer as action
. Then the reducer follows the switch case logic case types.PET
and returns the new state: the current color and the pet contained in the action, {color: 'black', pet: 'dog'}
We typically use constants to identify the
type
for the switch case (e.g.PET
orCOLOR
) to avoid typos and to make it easier to change in the future if needed.
useState vs. useReducer
These hooks are conceptually similar: they both store state variables. So, when should we use one or the other?
Consider the example above. If we were to use useState
in this scenario, what would it look like? One option would be two state variables:
This option works well when our state variables are independent: e.g. changing the pet will never also change the color, or vice versa. However, when we have multiple state variables that depend on one another, it can become challenging to keep everything in sync.
Another option would be a single object that we update manually:
This option lets us update all state variables at once, making it easier to keep things in sync. This option works well when our state object is small, but as soon as it becomes larger, our code for updating the state will start taking up a lot of lines, making it harder to follow the logic of our component.
The useReducer
hook essentially takes this second approach (a single state object), and provides a more structured API. We can then move our state-handling code outside of our component, so our component logic is clearer and our state logic can be tested independently. Additionally, if we want to allow children components to update the state, we can pass the dispatch
function down into children as a prop, rather than creating a separate callback prop for every possible change a child might want to make.
In summary:
- Use
useState
for "simple" state, like JavaScript primitive values - Use
useReducer
when there are different sub-values that depend on one another
The third argument: Initializer
We mentioned this uncommon option briefly above: useReducer
allows us to initialize state lazily with an initializer
function. This is useful when creating the initial state is computationally expensive.