React

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);

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);

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);

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.

Disabled

Whereas with useReducer, such code will not require an arrow function and the child component will not rerender if it uses memoization.

Enabled

For useState to be equivalent in this situation, we would have to use useCallback.

Disabled

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.

Rich 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 setter

useReducer 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.

Enabled

useReducer 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.

Renders: 1

useReducer 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.

0

Any 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.

The 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.