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.


    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 action
    • initialState - any initial state value, just like useState
    • 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.


    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 or COLOR) 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.