React.js: Why you might need useReducer
Tags: react, webdev, frontend
Published: 2023-07-05 07:50:10

Hooks in React.js is at the core of building functional components. Among the most used are the state management hooks, or simply the state hooks.

In React, two hooks allow you to manage the state:
- `useState` - Gives you a state variable and a function to change the value of the state
- `useReducer` - Gives you a state variable and a `dispatch` function for updating the state (via another function called a `reducer` function).

Both hooks do the same thing, "to manage state", and what you can do with `useState` can also be done with `useReducer` and vice versa.

So how do they differ?

The `useState` hook is the simpler one and is more commonly used. Meanwhile, `useReducer` (which is based on [Redux](https://redux.js.org/) - a state management library) is often mocked to have tons of boilerplate code.

But treating these hooks this way doesn't give `useReducer` justice.

There are use cases why `useReducer` makes a better option than `useState`.

In this article, I will discuss why you might want to consider using `useReducer` in some cases over the `useState` hook.

## Organizing the logic for changing the state

When managing a complex component state, your update logic might look messy, especially if you have different ways on changing the state.

I'll give you a simple example - a counter component. Let's say you need to build this component with the following requirements:
- the component should have a state for tracking the `count`
- you should be able to increment, decrement, as well as reset the count.
- when the count value is 10 or more, the increment and decrement value must be 10.
- when the count value is -10 or less, the increment and decrement value must be 5.
- when the count value is between -9 and 9 (inclusive), the increment and decrement value is just 1
- Resetting the state reverts the `count` value to `0`

So in this example component, we're required to have 3 update logics to the state (increment, decrement, reset) where both increment and decrement update logics have 3 different conditions.

With `useState`, you might do it this way:

```tsx
import { useState } from "react";

function Counter() {
    const [count, setCount] = useState(0);


    // The count update logics:
    // handleIncrement, handleDecrement, and handleReset

    const handleIncrement = () => {
        let incrementValue: number;

        if (count >= 10) {
            incrementValue = 10;
        } else if (count <= -10) {
            incrementValue = 5;
        } else {
            incrementValue = 1;
        }

        setCount(prevCount => prevCount + incrementValue);
    }

    const handleDecrement = () => {
        let decrementValue: number;
        
        if (count >= 10) {
            decrementValue = 10;
        } else if (count <= -10) {
            decrementValue = 5;
        } else {
            decrementValue = 1;
        }

        setCount(prevCount => prevCount - decrementValue)
    }

    const handleReset = () => {
        setCount(0);
    }


    return (
        <div>
            <p>Count: {count}</p>
            <button onClick={handleIncrement}> + </button>
            <button onClick={handleDecrement}> - </button>
            <button onClick={handleReset}>Reset</button>
        </div>
    )
}
```

So in this simple component, you'll see how convoluted the component can get because of those update logics. You may reason out that you can simplify the state update logic by refactoring those three separate functions into one function that accepts a parameter to tell whether you want to increment, decrement, or reset. Sure that works, but how does that differ from creating a `reducer` function?

And this is why you might consider using `useReducer`. With `useReducer`, you can work with complex state management in a more organized way. The main reason why `useReducer` would be a great choice is that you have a complex state where you have different update logics, and you want to keep things organized. This makes it easier to debug the update logics later (or update them when the requirements change).

Alright, let's rewrite the same component but let's use the `useReducer` hook here. Let's put the reducer logic in another file and just import it on the component.

```tsx
// counterReducer.ts

interface CounterAction {
    type: "increment" | "decrement" | "reset";
}

export interface CounterState {
    count: number;
}

export function counterReducer(state: CounterState, action: CounterAction) {
    const changeValue = state.count >= 10
                            ? 10
                            : state.count <= -10 ? 5 : 1;

    switch (action.type) {
        case "increment":
            return { ...state, count: state.count + changeValue };
        case "decrement":
            return { ...state, count: state.count - changeValue };
        case "reset":
            return { count: 0 };
        default:
            const _impossible: never = action.type;
            throw new Error(`Invalid action type: ${action.type}`);
    }
}

```
Now, let's import this in the counter component:

```tsx
// Counter.tsx

import { useReducer } from "react";
import { counterReducer, CounterState } from "./counterReducer"

const initialState: CounterState = { count: 0 };

function Counter() {
    const [state, dispatch] = useReducer(counterReducer, initialState);

    return (
        <div>
            <h1>Count: {state.count}</h1>
            <button onClick={() => dispatch({ type: "increment" })}> + </button>
            <button onClick={() => dispatch({ type: "decrement" })}> - </button>
            <button onClick={() => dispatch({ type: "reset" })}> Reset </button>
        </div>
    )
}

```

So in this case, the reducer function handles all the update logic to the state. This way, you can abstract away from the nitty-gritty details of the state update logic and just specify in the component which update logic you want to take effect. And since we separated the reducer logic in another file, the `Counter` component here no longer needs to concern itself with the update logic, making things more organized. If you need to debug the update logic, it's easier to know where to look. When requirements change and you want to update these state update logic, it's easier to go to what you're looking for and make changes.

Now you might argue that this makes more boilerplate code (especially because I used TypeScript which adds more boilerplate), but eliminating boilerplate code is not the goal of `useReducer` (because it's definitely more verbose). Rather, you should use `useReducer` if you have to manage a complex state and you want the state update logic organized. And as mentioned earlier, organizing your state update logic this way makes it easier to debug and update later when requirements change.

## Final thoughts
Hopefully, I conveyed to you the importance of `useReducer` and why you might want to use it in some cases over `useState`.

For further reading, I suggest you to look at [extracting state logic into a reducer](https://react.dev/learn/extracting-state-logic-into-a-reducer) from React documentation. There's also a challenge at the end to help you better understand `useReducer`.