UseReducer is Hard Work.

Table of Contents

The basics

const [state, dispatch] = useReducer(reducer, initialArg, init?)

First, what the f**k is a reducer?

A function that takes state and action as arguments and returns the next state of the app.

That definition isn't super helpful.

A reducer in React is just a function that takes the current state and an action, then returns a new state. Here's an example:

function reducer(state, action) {
    switch (action.type) {
        case "increment":
            return { count: state.count + 1 };

        case "decrement":
            return { count: state.count - 1 };

        default:
            return state;
    }
}

If React sends this action:

{ type: "increment" }

The reducer function thinks

Okay, I'll increase the count by 1

Why would I use this instead of useState?

A good rule of thumb is simple state = useStatecomplex state = useReducer.

With useReducer, instead of directly changing state like this:

setCount(count + 1);

You dispatch "actions" to the reducer, and it decides what the new state should be, e.g.:

dispatch({ type: "increment" });

UseReducer's parameters

const [state, dispatch] = useReducer(reducer, initialArg, init?)
  • reducer: The reducer function that specifies how the state gets updated. It must be pure, should take the state and action as arguments, and should return the next state. State and action can be of any types.
  • initialArg: The value from which the initial state is calculated. It can be a value of any type. How the initial state is calculated from it depends on the next init argument.
  • (Optional) init: The initializer function that should return the initial state. If it's not specified, the initial state is set to initialArg. Otherwise, the initial state is set to the result of calling init(initialArg).

What does it return?

useReducer returns an array of two values:

  1. The current state
  2. The dispatch function that lets you update the state to a different value and trigger a re-render.

Things to remember

A reducer must be a pure function. What is a pure function?, a pure function is a function that relies solely on its input parameters, and does not take into account any outside variables or state in returning a value.

A pure function must also not mutate its arguments.

This is pure:

function addNumbers(numberOne, numberTwo) {
    return numberOne + numberTwo;
}

This is impure:

const numberToAdd = 3;

function addNumbers(numberOne) {
    return numberOne + numberToAdd;
}

It is also worth remembering that a pure function must never mutate its arguments, e.g.:

const [questions, setQuestions] = useState([]);

function addQuestion(currentState, newQuestion) {
    currentState.push(newQuestion);
    return currentState;
}

addQuestion(questions, 'new question to add');

This is impure because it is directly mutating the questions state, by pushing to it.

Instead, do this:

const [questions, setQuestions] = useState([]);

function addQuestion(currentState, newQuestion) {
    return [...currentState, newQuestion];
}

addQuestion(questions, 'new question to add');

A real-world example

Forms are a great place to make use of useReducer, as they often have complex state objects that need to be managed efficiently.

const initialFormState: FormStateType = {
    name: '',
    email: ''
}

interface FormStateType {
    name: string;
    email: string;
}
interface ActionType {
    type: string;
    field: string;
    value: string;
}

function formDataReducer(state: FormStateType, action: ActionType) {
    switch (action.type) {
        case 'FIELD_CHANGED':
            return {
                ...state,
                [action.field]: action.value
            }
        case 'FORM_CLEARED':
            return {
                ...initialFormState
            }
        default:
            return {
                ...state
            }
    }
}

const [formData, dispatchFormData] = useReducer(formDataReducer, initialFormState);

This is a textbook example. Here, we're able to update form fields and clear the form (and add any other logic we want), in a single reducer function.

Here's an example of what the form component's code would look like:

<form action="">
    <input 
        type="text" 
        name="name" 
        id="name"
        value={formData.name}
        onChange={e =>
            dispatchFormData({
                type: 'FIELD_CHANGED',
                field: 'name',
                value: e.target.value,
            })
        }
    />
    <input 
        type="email" 
        name="email" 
        id="email"
        value={formData.email}
        onChange={e =>
            dispatchFormData({
                type: 'FIELD_CHANGED',
                field: 'email',
                value: e.target.value,
            })
        }
    />
</form>