You don't know React.useReducer
6 min read
useReducer is a built-in state management React hook, provided as an advanced alternative to useState. It is most commonly used in combination with useContext to emulate Redux without bringing in another dependency. Nothing inherently ties useReducer
to useContext
, however.
In its first form, you pass the reducer and its initialState
. Unlike Redux, the second initialState
parameter must be used. state = initialState
in the reducer
is frowned upon as the initialState
is often dependent on other hooks or props, which would prevent the reducer
function from being hoisted outside the component.
const reducer = (state, action) => newValue;
const [state, dispatch] = useReducer(reducer, initialState);
const reducer = (state, action) => newValue;
const [state, dispatch] = useReducer(reducer, initialState);
In its second form, an additional initInitialState
function is passed. This can be used to reuse the logic behind initInitialState
inside the reducer (for example, in a ‘reset’ action) or when initialState
is expensive to calculate every time the function renders. initialStateSeed
is passed to initInitialState
when the component is first rendered
const initInitialState = (initialStateSeed) => initialState;
const reducer = (state, action) => newValue;
const [state, dispatch] = useReducer(reducer, initialStateSeed, initInitialState);
const initInitialState = (initialStateSeed) => initialState;
const reducer = (state, action) => newValue;
const [state, dispatch] = useReducer(reducer, initialStateSeed, initInitialState);
The Redux convention where the initialState
is initialized inside the reducer
can be emulated by specifying the same reducer
as the initInitialState
.
const reducer = (state = initialState, action) => {
switch (action?.type) {
// ...
default:
return state;
}
};
const [state, dispatch] = useReducer(reducer, undefined, reducer);
const reducer = (state = initialState, action) => {
switch (action?.type) {
// ...
default:
return state;
}
};
const [state, dispatch] = useReducer(reducer, undefined, reducer);
What sets useReducer
apart from useState
’s setValue
(where [value, setValue] = useState()
) is that the dispatch
function both a stable identity and can perform custom logic when called. setValue
also has a stable value, but it always sets the state of that hook slot to its first received argument and cannot be modified to do something else, often requiring it to be wrapped in an arrow function, which then breaks the memoization rerender optimization as the function passed to a child component will always have a different identity.
Whereas with useReducer
, such code will not require an arrow function and the child component will not rerender if it uses memoization.
For useState
to be equivalent in this situation, we would have to use useCallback
.
useReducer
lends itself very well to situations where it can calculate the next state based on the current state, such as toggling a boolean or incrementing a number.
Practical use cases
Read section Practical use casesOpen/closed
Read section Open/closedRich webapps often feature dialogs, flyouts, popovers, and similar components which are opened by e.g. a button and provide their own onClose
handler. useReducer
is especially well suited for this use case, as these components can be expensive to render.
Toggle with explicit setter
Read section Toggle with explicit setteruseReducer
is primarily useful here when a UI element uses the toggle
by reference and an effect or callback uses the explicit setter form, in which the toggle function does not need to be memoized.
One way setter
Read section One way setteruseReducer
is also a great choice for a one way toggle. Although unlikely to matter in this particular use case, useReducer
also does a better job at preventing unnecessary rerenders when the new value is the same as the old value.
Incrementing numbers
Read section Incrementing numbersuseReducer
is best used here when the number is incremented one way. This example highlights it being possible to explicitly pass a number at the same time; in that case there isn’t much of a difference compared to useState
.
DOM inputs
Read section DOM inputsAny input that uses a { value, onChange }
based API is a great fit for useReducer
when onChange
does not return the same type as value
, like with a DOM input that instead returns an Event
, in which the new value needs to be unwrapped. Custom inputs whose onChange
returns the same type as value
can simply leverage useState
; its setValue
setter can be passed as-is in that case.
Force update
Read section Force updateThe signature of useReducer
is [value, dispatch]
. By indexing [1]
, we access dispatch
. The reducer here always returns a different value by identity (Object.is
), thus always resulting in a rerender after dispatch (named forceUpdate
here) is called.
Force updates are particularly useful when working with refs in performance sensitive-code. Changing the value of a ref does not result in a rerender. The forceUpdate function can be used to force the component to rerender when necessary.