The React useReducer hook

Published March 02, 2022

The React useReducer hook is an alternative to the useState hook. The useReducer hook is preferable over the useState hook when dealing with more complex component state logic. Using the useReducer hook is similar to using reducers in Redux.

useReducer versus useState

Using the useState hook to manage more complex state can result in a significant amount of code directly in your component body. An example of this is using useState to manage a list of items that needs to support the addition, modification, and deletion of items.

This complex state management logic should be managed separately from the component, who's main concern should only be to display the list of items. Mixing complex state management logic with presentation logic in the same component can make that component harder to test and to maintain over time.

The useReducer hook helps us to bring a greater separation of concerns to our component. It does this by allowing us to extract the state management logic outside of our component.

useReducer

const [state, dispatch] = useReducer(reducer, initialState);

The useReducer hook takes two arguments:

  • A reducer: a pure function that updates the state in an immutable way and returns the new state.
  • An initial state: the value that the state is initialized with.

The useReducer hook then returns an array of two elements:

  • The current state
  • A dispatch function: a special function that dispatches an action object.

What is an action object?

An action object is an object that is used to describe how the state should be updated.

An action object usually has a property type, which is a string that identifies what kind of operation the reducer must perform.

To create an action object to increment a counter in the state, we can do the following:

const action = {
  type: 'increment'
};

To create an action object to add a new item in a list, we can use the following action object:

const action = {
  type: 'add',
  payload: {
    name: 'Mario',
  },
};

In this case, we needed to pass an additional property into our action object so that we can add a new item with a specific name to our list. This additional property that is passed into an action object is often referred to as a payload. We named the property payload in this example, but it could have been called anything else, as long as it's a valid JavaScript object property name.

When we want to update the state, we call dispatch(actionObject) with the right action object. The action object will get forwarded to the reducer function which will update the state.

What is a reducer function?

Here's an example of a reducer function that handles the incrementing and decrementing of a counter.

function counterReducer(state, action) {
  const { type, payload } = action;

  switch (type) {
    case 'increment':
      return { 
        ...state,
        counter: state.counter + payload 
      };      
    case 'decrement':
      return { 
        ...state,
        counter: state.counter + payload 
      };
    default:
      return state;
  };
}

The reducer function is a pure function that does not directly modify the state using the state variable. Instead, it creates and returns a new state object. Avoid directly mutating the state using the state variable or things will end up not behaving as you might expect them to.

Here's an example of an action object for the dispatch function that will call the reducer function to increment the counter by 1.

const incrementAction = {
  type: 'increment',
  payload: 1,
};

Putting the reducer in its own file

We can create a new CounterReducer.js file and add our reducer function above in it. Within this file, we can also declare the initial state.

const initialCounterState = {
  counter: 0,
};

Let's also add declarations for our action objects within this file.

const incrementAction = {
  type: 'increment',
  payload: 1,
};

const decrementAction = {
  type: 'decrement',
  payload: 1,
};

Using the useReducer hook

We can now make use of the useReducer hook in a React component, which will make use of our reducer function.

import { useReducer } from "react";
import {
  counterReducer,
  initialCounterState,
  incrementAction,
  decrementAction,
} from '../reducers/CounterReducer.ts';

export const Counter = () => {
  const [state, dispatch] = useReducer(counterReducer, initialCounterState);

  return (
    <>
      <p>Counter: {state.counter}</p>
      <button onClick={() => dispatch(incrementAction)}>+</button>
      <button onClick={() => dispatch(decrementAction)}>-</button>
    </>
  );
}

When we use the useReducer hook within our Counter component, we provide it with two parameters:

  • Our reducer function that we called counterReducer.
  • The initial state that we called initialCounterState.

The dispatch function that we receive from the useReducer hook will allow us to forward our action objects to the reducer function. When the state is updated by the reducer function, the component will re-render, and the state variable that we get from useReducer will reflect the newly updated state.

Using TypeScript

We can also use TypeScript to add types for our state and action objects. To do so, we can save our CounterReducer.js as CounterReducer.ts and use the following code.

type State = {
  counter: number;
}

const initialCounterState: State = {
  counter: 0,
}

enum ActionType {
  Increment = 'INCREMENT',
  Decrement = 'DECREMENT',
}

type Action = {
  type: ActionType,
  payload: number,
}

const incrementAction: Action = {
  type: ActionType.Increment,
  payload: 1,
}

const decrementAction: Action = {
  type: ActionType.Decrement,
  payload: 1,
}

function counterReducer(state: State, action: Action): State {
  const { type, payload } = action;

  switch (type) {     
    case ActionType.Increment:
	    return { 
        ...state,
        counter: state.counter + payload 
      };
    case ActionType.Decrement:
      return { 
        ...state,
        counter: state.counter - payload 
      };
    default:
      return state;
  };
}

Conclusion

useReducer makes it easier to manage more complex state. The simple example above, where useReducer is used to manage a counter's state, was a trivial example only meant to demonstrate how to make use of the useReducer hook.

For simple state management within your components, you can stick to using the useState hook.