DEV Community

Cover image for Ditching useState and useReducer: Why useImmer is the better option
Rasaf Ibrahim
Rasaf Ibrahim

Posted on • Edited on

Ditching useState and useReducer: Why useImmer is the better option

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.

 

Table of Contents

 

In Case You're Not Familiar with useImmer

 

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.

 

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

npm install immer use-immer
Enter fullscreen mode Exit fullscreen mode

 

A simple example of useImmer:

/* 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>
  )
}
Enter fullscreen mode Exit fullscreen mode

 

⬆️ Back to the TOC

 

Why should we use useImmer instead of useReducer

 

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.

 

Import useImmer and Create the Functional Component

 

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

// functional component
function Cart() {

  // ... 

}
Enter fullscreen mode Exit fullscreen mode

 

Define the Initial State

 

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.

// initial state
const initialState = {
    items: [],
    shippingAddress: {
        street: '',
        city: ''
    }
}  
Enter fullscreen mode Exit fullscreen mode

 

Create the State

 

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

// Creating the state
const [cart, updateCart] = useImmer(initialState)
Enter fullscreen mode Exit fullscreen mode

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).

 

Create Actions

 

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.

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
        })
    }
}

Enter fullscreen mode Exit fullscreen mode

 

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.

 

Render the UI with JSX

 

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

<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>
Enter fullscreen mode Exit fullscreen mode

 

Advantages of Using useImmer over useReducer

 

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.

 

⚡️ 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.

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

<button onClick={() => updateCart(initialState)}>Clear Cart</button>
Enter fullscreen mode Exit fullscreen mode

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.

 

Full Code

 

// 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>
    )
}
Enter fullscreen mode Exit fullscreen mode

 

⬆️ Back to the TOC

 

When should we use useImmer?

 

🧨 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.

 

🧨 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.

 

🧨 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.

 

🧨 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.

 

⬆️ Back to the TOC

 

That's it. 😃 Thanks for reading. 🎉

Top comments (44)

Collapse
 
hakan_turan profile image
Hakan Turan

I gotta say, 'useImmer' looks way easier to use than 'useReducer'. Nice job on the article, dude!

Collapse
 
brense profile image
Rense Bakker • Edited

Do keep in mind that people tend to over-complicate useReducer by writing redux style reducer functions with switch statements. If you use the most simple reducer function, it works exactly like useImmer:

const initialState = {
  foobar: '',
  // some more state
}

function simpleReducer(prevState, nextState){
  return { ...prevState, ...nextState }
}

function SomeComponent (){
  const [state, setState] = useReducer (simpleReducer, initialState)
  const handleChange = useCallback ((e) => {
    setState({ foobar: e.target.value })
  }, [])
  return <input onChange={handleChange} value={state.foobar} />
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
rasaf_ibrahim profile image
Rasaf Ibrahim • Edited

It's definitely a more readable approach of using useReducer. You have also used useCallback to avoid unnecessary re-renders. However, with useImmer, useCallback is not necessary when you are not passing the state as props, because Immer uses a proxy object that keeps track of changes and only applies the changes when necessary. Additionally, with useImmer, you can write code in a mutable way.

Thread Thread
 
brense profile image
Rense Bakker

Apologies, I realize I made it sound like there is no use for useImmer, this was not my intention! Immer and useImmer hook are great. I just wanted to point out that people usually project all kinds of unnecessary redux madness onto the useReducer hook 😁

Collapse
 
fbolaji profile image
Francis.B

Thank you for pointing that out because people tend to do 'over-kill'

Collapse
 
rasaf_ibrahim profile image
Rasaf Ibrahim

Thank you. 🎉

Collapse
 
housi profile image
Sonia • Edited

I don't agree it's better. Also it's not 'more javascript-like' if it looks like mutable JavaScript code, but it's not. Details of what is going on are hidden from the coder. The fact is that you can avoid mutations explicitly in just few more keystrokes, which would be

*more readable
*more performant - you just return new object, instead of watching object for changes (however immer does it) and then returning new object anyway (for react to update ui)
*saving yourself and the planet few kb's×traffic of useless code transfer
*saving the planet again by less runtime computation = clients×runingtime less energy
*saving your fellow/future collaborators from extra complexity and upkeep of extra dependency

Totally bad idea to use it in my opinion.

Collapse
 
rasaf_ibrahim profile image
Rasaf Ibrahim

Thank you for sharing your opinion. I appreciate your feedback. However, I don't think useImmer is less performant than returning a new object explicitly. Because it only creates new objects for the parts of the state that have changed and reuses the rest. Also, immer uses structural sharing to optimize the comparison of the old and new state, which can improve the performance of React's rendering. You can read more about these features and benefits of immer in their documentation. Moreover, immer is very popular and widely used in some of the most popular and widely used state management libraries for React, such as redux toolkit and zustand. This shows that immer is a reliable and well-supported library that has been adopted by many developers and projects in the JavaScript community. Of course, you are free to use whatever tool or technique works best for you and your project.

Collapse
 
housi profile image
Sonia

I have never seen any use case for an extensive unpersistent state client-side. Local data should be stored in a cache and ui-related state (dropdowns etc ;p) is usually super simple and working perfectly with built-in 'useState'.

With each library we add to overall complexity and become less 'portable'

Removing such a library in case of compatibility problems, that may arise in the future, would be a nightmare, because this impacts ALOT.

I mean, this is very bold decision in a project, if we don't have performance issues, I'd say it's better to stick to standard. If you have, then moving data out of state should help :)

Collapse
 
mattbarnicle profile image
Matt Barnicle

Thanks for a well written introduction to useImmer. I wasn't aware of this library before. I've looked through the comments and see that there is some disagreement over whether or not this is a good library to use as opposed to useReducer and the arguments are worth considering. This post has given me some things to think about, but I might give it a try to see what I think of it.

Collapse
 
rasaf_ibrahim profile image
Rasaf Ibrahim

Thank you for your feedback. You can certainly give useImmer a try, especially for local state management and state of reusable component.

Collapse
 
lamarcke profile image
Lamarcke • Edited

Great article. I'm personally not a fan of mutable state.

How does useImmer handle batching? For example, updating a cart object field just after another update (inside the same function). Would it re-render two times or wait for the function to finish?

Edit: it seems like I was wrong, I wasn't introduced to Immer before so I thought it was managing mutable state, when in fact it allows you to manage immutable state by modifying only a draft version of your object:

With Immer, this process is more straightforward. We can leverage the produce function, which takes as first argument the state we want to start from, and as second argument we pass a function, called the recipe, that is passed a draft to which we can apply straightforward mutations. Those mutations are recorded and used to produce the next state once the recipe is done. produce will take care of all the necessary copying, and protect against future accidental modifications as well by freezing the data.

Collapse
 
rasaf_ibrahim profile image
Rasaf Ibrahim

Thanks for your comment. You are right about Immer's approach to state management, it allows for straightforward mutations while maintaining immutability.

Collapse
 
tbm206 profile image
Taha Ben Masaud

immer is possibly the worst thing that happened to the JavaScript world recently.

Collapse
 
rasaf_ibrahim profile image
Rasaf Ibrahim

Thanks for sharing your opinion. But I don't know why would think that immer is the worst thing that happened to the JavaScript world recently. What specific issues have you encountered or what do you think are the drawbacks of using immer?

Collapse
 
elr0berto profile image
elr0berto

I would guess that updating the state in a "mutable" manner would hide bugs where you accidentally mutate the state without meaning to. Where with reducer you would get an error or warning that you mutated the state.

Thread Thread
 
rasaf_ibrahim profile image
Rasaf Ibrahim

That's a good point but it is also possible to accidentally update the state immutably in a reducer, especially with complex or nested state objects.

On the other hand, Immer can help make your code more readable and make it harder to write incorrect code.

Thread Thread
 
elr0berto profile image
elr0berto

Ok, then I have no idea why he would say that immer is the worst. Maybe he is just trolling...

Collapse
 
tbm206 profile image
Taha Ben Masaud

immer encourages procedural style of programming. It's a regression from the more functional style of updating state.

If you ever used lenses in Haskell, or even those offered by ramda, you'll know how big of a regression immer is.

Collapse
 
leob profile image
leob

I think we're all extremely eager now to know why that is so ... care to explain?

Collapse
 
tbm206 profile image
Taha Ben Masaud

It simply encourages a more procedural and mutable style of programming.

It also forces you to hold several pieces of state in your mind.

Compare this with lenses.

Thread Thread
 
leob profile image
leob • Edited

I think it simply allows people to write the code they would naturally write - the kind of "immutable" code you'd write in more complicated scenarios is anything but natural ... and the resulting code is more concise, easier to understand, and easier to maintain - only pluses in my book.

(I don't see how it forces you to "keep more pieces of state in your mind", when all it does is let you write the same piece of code, accomplishing the same thing, but more concisely - and of course the underlying assumption is that you have sufficient comprehension of how this works so that you know that under the hood you're still in fact writing immutable code - it's just a tool to accomplish that more simply)

Thread Thread
 
tbm206 profile image
Taha Ben Masaud

I'm not sure what you mean by natural way of writing code: or if that at all exists?

I'd disagree that the way immer allows state updates is in anyway concise.

It forces to think about different pieces of state individually; thus keeping several things in mind. On the other hand, a functional programming style allows you to think of state as data pipeline: one piece a time.

Thread Thread
 
leob profile image
leob

maybe yes, maybe no, at this point it's not really tangible

Collapse
 
danbowling profile image
Dan Bowling

What stops someone from using UpdateCart outside of the actions?

Great work on this article!!!!

Collapse
 
rasaf_ibrahim profile image
Rasaf Ibrahim

Actually, it's not possible to stop someone from updating the state outside the defined actions. However, this is not unique to useImmer. I learned from a comment in this article that it's possible to update the state with useReducer too without defining actions. If you look through the comments, you'll find the following code:

const initialState = {
  foobar: '',
  // some more state
}

function simpleReducer(prevState, nextState){
  return { ...prevState, ...nextState }
}

function SomeComponent (){
  const [state, setState] = useReducer (simpleReducer, initialState)
  const handleChange = useCallback ((e) => {
    setState({ foobar: e.target.value })
  }, [])
  return <input onChange={handleChange} value={state.foobar} />
}
Enter fullscreen mode Exit fullscreen mode

So, ultimately it is a matter of personal preference whether to only use defined actions for a more structured approach or not.

Collapse
 
danbowling profile image
Dan Bowling

Thanks for the reference!

Collapse
 
asupanogtong profile image
Ruel Escano asupan

I can't understand all sir sorry all I know is call and text thank I'm tired

Collapse
 
rasaf_ibrahim profile image
Rasaf Ibrahim • Edited

Thank you for your feedback. Actually, I didn't provide any syntax for both the useState and useReducer hooks, assuming that readers are already familiar with them. I am not sure, but I am guessing that if someone is not completely familiar with these hooks, specially useReducer, it can be challenging to understand the comparison that I have made with useImmer.

Collapse
 
fbolaji profile image
Francis.B

Well explained and good demo.

Collapse
 
rasaf_ibrahim profile image
Rasaf Ibrahim

Thank you. 🎉

Collapse
 
chrismaganga profile image
Chrismaganga

useImmer seems the best way to go

Collapse
 
liamjoneslucout profile image
liam-jones-lucout

Never been a big fan of useReducer. Will be looking at this for my projects. Cheers.