The beginner's guide to the React useReducer Hook

Delivered in React 16.8 useReducer hook allows you to manage complex state logic in React Applications. The useReducer hook is an alternative for the useState hook and, combined with other interesting React’s feature called Context, helps manage state efficiently.

Usually, Redux or MobX are the best options for managing state in large React applications, but sometimes using 3rd party libraries is unnecessary and can kill your project.

If you have a small project, consider using native React hooks instead of injecting extensive 3rd party libraries that add a lot of code and force you to use their architecture and approach in your project.

On the other hand, using GraphQL with the Apollo client, you use Apollo’s state management mechanisms, and using another library for state management is redundant.

Understand useReducer

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

The useReducer hook receives two params:

  • Reducer function - a pure function that return state depends on dispatched action
  • Initial state - initial state (how can I explain that?) ;-)

The useReducer returns array that contains:

  • Current state - initially, this will be the initial state that you passed to a reducer, and after each action dispatch, the state is changed and returned here.

  • Dispatch function - a function that receives action as an argument and dispatches this action in a reducer.

Note: state in reducers is immutable. It cannot be changed outside the reducer, and also keep in mind that you cannot mutate state in reducer when the action “working.”

When you want to change the state, you need to copy the current one, then mutate the copy, and at the end, return that replicated state as the current state.

Reducer function

Take a look at the sample reducer function:

const ADD_MESSAGE =ADD_MESSAGE;
const REMOVE_MESSAGE =REMOVE MESSAGE;

export function MessagesReducer(state, action) {
    switch(action.type) {
        case ADD_MESSAGE: {
            return {
                messages: [
                    ...state.messages,
                    action.message
                ]
            };
        }
        case REMOVE_MESSAGE: {
            const indexToToRemove = state.messages.indexOf(action.message);

            if (indexToToRemove >= 0) {
                return {
                    messages: [
                        ...state.messages.splice(indexToToRemove, indexToToRemove)
                    ]
                }
            } else {
                return state;
            }
        }

        default: {
            return state;
        }
    }
}

The reducer function receives two parameters: state and action. The state is data that we are manipulating, and action determines what we want to do with that data.

Action determines what we want to do with data, and usually, an action is an abject with one obligatory field: “type.”

In the Reducer above, we can see two actions that we handle:

  • ADD_MESSAGE
  • REMOVE_MESSAGE

When we want to dispatch those actions, we have to pass these objects to the reducer function:

{type: ADD_MESSAGE, message}; // adding message
{type: REMOVE_MESSAGE, message}; // removing message

As you can see, I passed the second parameter there: “message,” and I have access to it in the reducer function because an action is just an object, and I can grab a message by typing: action.message.

We handle two actions in the reducer. Switch statement checks action. type value and try to match it with any case. If any case does not address the taken type, then default case is used, and the current state is returned:

default: {
     return state;
}

The first case in reducer is ADD_MESSAGE:

case ADD_MESSAGE: {
    return {
        messages: [
            ...state.messages,
            action.message
        ]
    };
}

This case returns a new array contains the current statemessages array (state.messages) and a new message received in action (action.message).

The second case is REMOVE_MESSAGE:

case REMOVE_MESSAGE: {
    const indexToToRemove = state.messages.indexOf(action.message);

    if (indexToToRemove >= 0) {
        return {
            messages: [
                ...state.messages.splice(indexToToRemove, indexToToRemove)
            ]
        }
    } else {
        return state;
    }
}

It also receives a message object in action, and the reducer checks if the received message exists in the current state. If the indexToRemove const is equal or greater than zero, then the reducer function returns a new state containing messages without a message that should be removed.

Otherwise, the reducer returns the current state without any mutations.

Dispatch function

The dispatch function is used to dispatching actions to reducers. It accepts an object that specifies the action type.

The useReducer hook returns the dispatch function, and then you can use it in a component to mutate a state. Take a look at the example below:

<button onClick={() => dispatch({type: ADD_MESSAGE, message: ‘React is cool!’’})}> Add message </button>

A good practice is to wrap an action object by a method that returns this object. Usually, I create actions where I have declared a reducer and export them to use in components.

export const addMessage = message => {
    return {type: ADD_MESSAGE, message};
}

export const removeMessage = message => {
    return {type: REMOVE_MESSAGE, message};
}

Then I can use those actions like this:

import {removeMessage as removeMessageAction from./messagesReducer’;

dispatch(removeMessageAction(message))

Of course, you can pass the dispatch function to child components and use it there like this:

<ChildComponent addMessage={message => dispatch(removeMessageAction(message))}/>

Example of using the useReducer

Take a look at the complete example of the reducer called MessagesReducer. It’s responsible for managing messages (notifications) in an app.

The reducer

const ADD_MESSAGE = 'ADD_MESSAGE';
const REMOVE_MESSAGE = 'REMOVE_MESSAGE';

export function MessagesReducer(state, action) {
    switch(action.type) {
        case ADD_MESSAGE: {
            return {
                messages: [
                    ...state.messages,
                    action.message
                ]
            };
        }
        case REMOVE_MESSAGE: {
            const indexToToRemove = state.messages.indexOf(action.message);

            if (indexToToRemove >= 0) {
                return {
                    messages: [
                        ...state.messages.splice(indexToToRemove, indexToToRemove)
                    ]
                }
            } else {
                return state;
            }
        }

        default: {
            return state;
        }
    }
}

export const messagesInitialState = { messages: [] }

export const addMessage = message => {
    return {type: ADD_MESSAGE, message};
}

export const removeMessage = message => {
    return {type: REMOVE_MESSAGE, message};
}

Using The reducer in the Context

In this particular example, I used that reducer in the Context. Take a look:

import React, { createContext, useReducer } from 'react';
import {
    MessagesReducer,
    messagesInitialState,
    addMessage as addMessageAction,
    removeMessage as removeMessageAction
 } from '../../reducers/Messages';

export const MessagesContext = createContext();

export const MessagesProvider = ({ children }) => {
    const [{ messages }, dispatch ] = useReducer(MessagesReducer, messagesInitialState);

    const removeMessage = message => dispatch(removeMessageAction(message));
    const addMessage = message => dispatch(addMessageAction(message));

    return <MessagesContext.Provider value={{
        messages,
        addMessage,
        removeMessage
    }}>
        {children}
    </MessagesContext.Provider>
};


`

You can find the complete example of using that reducer in the pull request of my personal project here.

useState or useReducer?

You might ask a question: “when should I use the useReducer hook, and when the useState?”

Probably it depends on you. Just keep in mind that useReducer is better for managing complex states.

I use the useState hook for managing primitive states like strings, numbers, and booleans.

When I have to manage a more complex state, I prefer to use the useReducer hook.

Summary

The useReducer hook is a good alternative for 3rd party libs like Redux and ModX. Also is an excellent option to handle non-GraphQL states in a React app connected with GraphQL API using Apollo client.

Combining the useReducer hook with other React’s mechanism called Context allows you to manage state in your app efficiently.

Want to go deeper?

Today I showed you only the basics of the useReducer hook. If you want to go deeper, take a look at these sources:

12