Ditching useState and useReducer: Why useImmer is the better option
Tags: react, hook, useimmer, webdev
Published: 2023-02-25 16:41:29



In React, `useState` and `useReducer` are commonly used for managing state. While `useImmer` is a popular alternative to `useState`, in this article, we will explore how it can also serve as a simpler and more effective alternative of `useReducer`.


&nbsp;

#### Table of Contents
- [In Case You're Not Familiar with useImmer](#in-case-youre-not-familiar-with-useimmer)
- [Why should we use useImmer instead of useReducer](#why-should-we-use-useimmer-instead-of-usereducer)
- [When should we use useImmer?](#when-should-we-use-useimmer)



&nbsp;

### In Case You're Not Familiar with useImmer

&nbsp;


`useImmer` is a custom React hook. It's similar to `useState` but offers some distinct advantages, especially when it comes to managing complex state. With `useImmer`, you can update the state as if it were directly mutable, similar to regular JavaScript. It is possible because, behind the scene `useImmer` uses the `Immer` library to create a new immutable copy of the state.

&nbsp;

To use `useImmer`, first, you need to install it by running the following command in your terminal:

```shell
npm install immer use-immer
```

&nbsp;

A simple example of `useImmer`:


```js
/* importing useImmer */
import { useImmer } from 'use-immer';

/* functional component */
function Count() {

  /* Declaring a new state variable using the useImmer hook.
    Here, we are initializing our state with a single
    property called "count" with an initial value of 0 */

  const [state, updateState] = useImmer({ count: 0 });


  /* Defining a function that will modify our state */
  const increment = () => {
    /* calling the updateState function and passing as
       it a draft */
    updateState(draft => {
      /* Here we are directly modifying the draft as 
        if it were mutable */
      draft.count += 1;
    })
  }


  /* Defining another function that will modify our state */
  const decrement = () => {
    /* Calling the updateState function and pass it a draft */
    updateState(draft => {
      /*Here we are directly modifying the draft as 
        if it were mutable */
      draft.count -= 1;
    })
  }


  /* Rendering the UI using the state */
  return (
    <div>
      <h3>Count: {state.count}</h1>
      <button onClick={increment}>Increment</button>
      <button onClick={decrement}>Decrement</button>
    </div>
  )
}
```

&nbsp;

[⬆️ Back to the TOC](#table-of-contents)


&nbsp;

### Why should we use useImmer instead of useReducer

&nbsp;


We will explore an example of `useImmer` that follows the `useReducer` pattern. We will create state and actions and use them in a similar way as `useReducer`, but with useImmer's increased readability, flexibility, and mutability. Through this example, we will see why `useImmer` should be considered more than the `useReducer`.


&nbsp;

**Import useImmer and Create the Functional Component**

&nbsp;


```js
// import useImmer
import { useImmer } from 'use-immer';

// functional component
function Cart() {

  // ... 

}
```


&nbsp;

**Define the Initial State**

&nbsp;


When using `useReducer`, it's common practice to define the initial state before proceeding. Similarly, we will use this approach with the `useImmer` hook. To do this, we'll create a state that is somewhat complex and resembles the state you would typically define with `useReducer`.


```js
// initial state
const initialState = {
    items: [],
    shippingAddress: {
        street: '',
        city: ''
    }
}  
```



&nbsp;

**Create the State**

&nbsp;


We have defined our initial state. Now, we can create our state using the `useImmer` hook.



```js
// Creating the state
const [cart, updateCart] = useImmer(initialState)
```

The `useImmer` hook returns an array containing two values: the current state (`cart` in this example), and a function that we can use to update the state (`updateCart` in this example).





&nbsp;

**Create Actions**

&nbsp;

When using the `useReducer` hook, it's mandatory to define actions in order to update the state. Similarly, we will create actions for `useImmer` to resemble the same pattern and achieve predictable state updates.


```js
const actions = {

    // Add an item to the cart
    addItemToCart: (payload) => {

        const { item } = payload

        updateCart(draft => {
            draft.items.push(item)
        });
    },

    // Remove an item from the cart
    removeItemFromCart: (payload) => {

        const { itemIndex } = payload

        updateCart(draft => {
            draft.items.splice(itemIndex, 1)
        })
    },

    // Update the shipping address of the cart
    updateShippingAddress: (payload) => {

        const { shippingAddress } = payload

        updateCart(draft => {
            draft.shippingAddress = shippingAddress
        })
    }
}
    
```

&nbsp;

Defining actions using the above method provides two advantages over `useReducer`:


⚡️ Mutability: With `useImmer`, we have the power of mutable updates which allows for less code and more JavaScript-like code. This is in contrast to `useReducer` which requires a more functional programming approach and immutable updates. By using mutable updates in `useImmer`, you can achieve the same result with fewer lines of code, making it easier to write and maintain.


⚡️ Readability: Compared to `useReducer`, where the actions are typically defined as a `switch` case in a reducer function, the `useImmer` approach can be more readable as each action is a separate function with a clear and concise name. 


&nbsp;

**Render the UI with JSX**

&nbsp;

Finally, we can use the `cart` state and the `actions` object in the JSX to render the UI.

```js
<div>

    {/* Displaying the changes of the 'cart' state */}
    <p>Cart State: {JSON.stringify(cart)}</p>

    {/* Display a list of items in the cart */}
    <ul>
        {cart.items.map((item, index) => (
            <li key={index}>
                {item.name} - ${item.price}

                {/* Call the removeItemFromCart action when the remove button is clicked */}
                <button onClick={() => actions.removeItemFromCart({ itemIndex: index })}>Remove</button>
            </li>
        ))}
    </ul>

    {/* Call the addItemToCart action when the add item button is clicked */}
    <button onClick={() => actions.addItemToCart({ item: { name: 'Product', price: 9.99 } })}>
        Add Item
    </button>


    {/* Allow the user to update the shipping address */}
    <div>

        <h4>Shipping Address:</h4>

        <input type="text" placeholder="Street"
            value={cart.shippingAddress.street}
            onChange={(e) => actions.updateShippingAddress({ shippingAddress: { ...cart.shippingAddress, street: e.target.value } })}
        />

        <input type="text" placeholder="City"
            value={cart.shippingAddress.city}
            onChange={(e) => actions.updateShippingAddress({ shippingAddress: { ...cart.shippingAddress, city: e.target.value } })}
        />

    </div>


</div>
```



&nbsp;

**Advantages of Using useImmer over useReducer**

&nbsp;

We've already explored two advantages(Mutability & Readability) of using useImmer over useReducer in the `Create Actions` section. However, there's one more significant advantage worth discussing, which we'll cover in this section.


&nbsp;

⚡️ Flexibility: With useImmer, it's possible to update the state object outside of the defined actions. This is not possible in useReducer, and can be particularly helpful in certain situations where you need more flexibility in updating the state.


&nbsp;
Here's an example of how to clear the cart using `useImmer` without defining a new action:


```js
<button onClick={() => updateCart(initialState)}>Clear Cart</button>
```

This button component will reset the `cart` state back to its initial state and clear all items in the cart. With `useImmer`, we can update the state object directly in this ad-hoc manner without the need for an action to be defined. This is not possible with `useReducer`, where all state updates must be dispatched through the defined actions.


&nbsp;

**Full Code**

&nbsp;


```js
// import useImmer
import { useImmer } from 'use-immer';


// functional component
export default function Cart() {

    // Define the initial state of the cart
    const initialState = {
        items: [],
        shippingAddress: {
            street: '',
            city: ''
        }
    };

    // Call the useImmer hook to create a cart state 
    const [cart, updateCart] = useImmer(initialState);

    // Define a set of actions that can be used to update the cart state
    const actions = {

        // Add an item to the cart
        addItemToCart: (payload) => {

            const { item } = payload

            updateCart(draft => {
                draft.items.push(item)
            });
        },
    
        // Remove an item from the cart
        removeItemFromCart: (payload) => {

            const { itemIndex } = payload

            updateCart(draft => {
                draft.items.splice(itemIndex, 1)
            })
        },
    
        // Update the shipping address of the cart
        updateShippingAddress: (payload) => {

            const { shippingAddress } = payload

            updateCart(draft => {
                draft.shippingAddress = shippingAddress
            })
        }
    }
    

    // Render the cart UI
    return (

        <div>

            {/* Displaying the changes of the 'cart' state */}
            <p>Cart State: {JSON.stringify(cart)}</p>

            {/* Display a list of items in the cart */}
            <ul>
                {cart.items.map((item, index) => (
                    <li key={index}>
                        {item.name} - ${item.price}

                        {/* Call the removeItemFromCart action when the remove button is clicked */}
                        <button onClick={() => actions.removeItemFromCart({ itemIndex: index })}>Remove</button>
                    </li>
                ))}
            </ul>

            {/* Call the addItemToCart action when the add item button is clicked */}
            <button onClick={() => actions.addItemToCart({ item: { name: 'Product', price: 9.99 } })}>
                Add Item
            </button>


            {/* Allow the user to update the shipping address */}
            <div>

                <h4>Shipping Address:</h4>

                <input type="text" placeholder="Street"
                    value={cart.shippingAddress.street}
                    onChange={(e) => actions.updateShippingAddress({ shippingAddress: { ...cart.shippingAddress, street: e.target.value } })}
                />

                <input type="text" placeholder="City"
                    value={cart.shippingAddress.city}
                    onChange={(e) => actions.updateShippingAddress({ shippingAddress: { ...cart.shippingAddress, city: e.target.value } })}
                />

            </div>

            {/* Call the updateCart function with the initial state to clear the cart */}
            <button onClick={() => updateCart(initialState)}>Clear Cart</button>

        </div>
    )
}
```


&nbsp;

[⬆️ Back to the TOC](#table-of-contents)



&nbsp;

### When should we use useImmer?

&nbsp;

🧨 Use `useImmer` for local state management and for creating reusable components 

> When managing local state or creating a reusable component that requires state management, `useImmer` is a suitable option.

&nbsp;

🧨 We can use `useImmer` for global state management, but there are other better options

>We can share the state created by `useImmer` with other components by using `createContext` and `useContext`. Therefore, we can use `useImmer` to create global state, but should we use it for global state management?
>
> When it comes to global state management, libraries like `redux toolkit` or `zustand` are more suitable.

&nbsp;


🧨 Even though other libraries may be better for global state management, useImmer is a better option for local state management and for creating reusable components


> Libraries like `redux` and `zustand` are created for global state management. If we use them for creating reusable components:
>
> -   The state will be polluted as multiple components can modify the same state, leading to unexpected behavior and bugs.
>
>On the other hand, if we use `useImmer` for reusable components:
>
> -   Each instance of the component will have its own independent state, separate from other instances of the same component. This ensures that the state of one instance does not affect the state of another instance.
>
> -   Since the state is contained within the component, there is no need to worry about the state being modified elsewhere in the application.

&nbsp;

🧨 Way to share the useImmer created state among other components


> For global state
> 
> - we can use `createContext` and `useContext` to share the state among other components.
>
> Local State and Reusable Components:
> - Suppose, we are using local state in a component or creating a reusable component. The component is becoming large, and so we have divided it into multiple child components. Now, in the parent component, we will create the state with `useImmer`. Then, we will share the state with the child components through props.


&nbsp;

[⬆️ Back to the TOC](#table-of-contents)


&nbsp;

That's it. 😃 Thanks for reading. 🎉