DEV Community

Cover image for Use Zustand and Immer to Manage the state of your React app
Francisco Mendes
Francisco Mendes

Posted on

Use Zustand and Immer to Manage the state of your React app

I believe that everyone at some point when they were working with nested data became so saturated that they wondered if there would be an easier solution.

I didn't go into great detail about what Immer.js is or what JavaScript immutability is, because there are amazing articles freely available on the internet that explain it better than I do.

If you are interested in the points mentioned in the previous paragraph, I recommend reading this article.

In this example I will use Inner.js with Zustand, however you can use it with many other state managers. I believe that after using Inner.js, you won't want anything else to work with objects and arrays in your state.

Today I'm going to do the sample code in a different way. This time I'm going to give the majority of the code (github repo here) and what I'm going to focus more on is our store, because what matters today is using Zustand with Immer.

Let's code

The idea of the project is to list the names of Korean dramas that we like or that we know. The features needed in the application and that we have to implement are the add, delete and edit mutations.

The initial code for our store is as follows:

// @src/store.js

import create from "zustand";

export const useStore = create((set) => ({
  kdramas: [
    {
      id: Math.floor(Math.random() * 100),
      name: "River Where the Moon Rises",
    },
    {
      id: Math.floor(Math.random() * 100),
      name: "The Crowned Clown",
    },
  ],
  // Mutations will go here
}));
Enter fullscreen mode Exit fullscreen mode

As you may have noticed, the initial state already has 2 elements and if you are running the application on port 3000 you should have a visual result similar to this:

web app

Let's start by implementing the add a new drama mutation, let's create an action called addDrama() that will receive a payload as a single argument.

Then we'll import the immer and we'll use the produce function so we can make a copy of our current state so we can make the respective mutations of it, like this:

// @src/store.js

import create from "zustand";
import produce from "immer";

export const useStore = create((set) => ({
  kdramas: [
    {
      id: Math.floor(Math.random() * 100),
      name: "River Where the Moon Rises",
    },
    {
      id: Math.floor(Math.random() * 100),
      name: "The Crowned Clown",
    },
  ],
  addDrama: (payload) =>
    set(
      produce((draft) => {
        // Logic goes here
        });
      })
    ),
  // More mutations will go here
}));
Enter fullscreen mode Exit fullscreen mode

Now instead of adding the new drama directly to our state, let's do it in the draft. The approach is very similar to vanilla JavaScript, we just have to use the .push() method.

// @src/store.js

import create from "zustand";
import produce from "immer";

export const useStore = create((set) => ({
  kdramas: [
    {
      id: Math.floor(Math.random() * 100),
      name: "River Where the Moon Rises",
    },
    {
      id: Math.floor(Math.random() * 100),
      name: "The Crowned Clown",
    },
  ],
  addDrama: (payload) =>
    set(
      produce((draft) => {
        draft.kdramas.push({
          id: Math.floor(Math.random() * 100),
          name: payload,
        });
      })
    ),
  // More mutations will go here
}));
Enter fullscreen mode Exit fullscreen mode

You should now be able to add a new Korean drama to the list. In this way:

adding new drama

Now we're going to create a new mutation, but this time we're going to eliminate a drama that's on the list. Let's name our action removeDrama(). Which will also have the payload as its only argument.

// @src/store.js

import create from "zustand";
import produce from "immer";

export const useStore = create((set) => ({
  kdramas: [
    {
      id: Math.floor(Math.random() * 100),
      name: "River Where the Moon Rises",
    },
    {
      id: Math.floor(Math.random() * 100),
      name: "The Crowned Clown",
    },
  ],
  addDrama: (payload) =>
    set(
      produce((draft) => {
        draft.kdramas.push({
          id: Math.floor(Math.random() * 100),
          name: payload,
        });
      })
    ),
  removeDrama: (payload) =>
    set(
      produce((draft) => {
        // Logic goes here
      })
    ),
  // More mutations will go here
}));
Enter fullscreen mode Exit fullscreen mode

First let's look for the index of the element in the array that has a key equal to our payload, which in this case is the id.

// @src/store.js

import create from "zustand";
import produce from "immer";

export const useStore = create((set) => ({
  kdramas: [
    {
      id: Math.floor(Math.random() * 100),
      name: "River Where the Moon Rises",
    },
    {
      id: Math.floor(Math.random() * 100),
      name: "The Crowned Clown",
    },
  ],
  addDrama: (payload) =>
    set(
      produce((draft) => {
        draft.kdramas.push({
          id: Math.floor(Math.random() * 100),
          name: payload,
        });
      })
    ),
  removeDrama: (payload) =>
    set(
      produce((draft) => {
        const dramaIndex = draft.kdramas.findIndex((el) => el.id === payload);
        // More logic goes here
      })
    ),
  // More mutations will go here
}));
Enter fullscreen mode Exit fullscreen mode

Then just remove the element from the array with its index.

// @src/store.js

import create from "zustand";
import produce from "immer";

export const useStore = create((set) => ({
  kdramas: [
    {
      id: Math.floor(Math.random() * 100),
      name: "River Where the Moon Rises",
    },
    {
      id: Math.floor(Math.random() * 100),
      name: "The Crowned Clown",
    },
  ],
  addDrama: (payload) =>
    set(
      produce((draft) => {
        draft.kdramas.push({
          id: Math.floor(Math.random() * 100),
          name: payload,
        });
      })
    ),
  removeDrama: (payload) =>
    set(
      produce((draft) => {
        const dramaIndex = draft.kdramas.findIndex((el) => el.id === payload);
        draft.kdramas.splice(dramaIndex, 1);
      })
    ),
  // More mutations will go here
}));
Enter fullscreen mode Exit fullscreen mode

In this way, we have already managed to eliminate an element that is present in the list, like this:

deleting a korean drama

Now we just need to implement and update a Korean drama that is present in the list. To do this, let's create a new action called patchDrama().

// @src/store.js

import create from "zustand";
import produce from "immer";

export const useStore = create((set) => ({
  kdramas: [
    {
      id: Math.floor(Math.random() * 100),
      name: "River Where the Moon Rises",
    },
    {
      id: Math.floor(Math.random() * 100),
      name: "The Crowned Clown",
    },
  ],
  addDrama: (payload) =>
    set(
      produce((draft) => {
        draft.kdramas.push({
          id: Math.floor(Math.random() * 100),
          name: payload,
        });
      })
    ),
  removeDrama: (payload) =>
    set(
      produce((draft) => {
        const dramaIndex = draft.kdramas.findIndex((el) => el.id === payload);
        draft.kdramas.splice(dramaIndex, 1);
      })
    ),
  patchDrama: (payload) =>
    set(
      produce((draft) => {
        // Logic goes here
      })
    ),
}));
Enter fullscreen mode Exit fullscreen mode

First let's try to find the array element with its id.

// @src/store.js

import create from "zustand";
import produce from "immer";

export const useStore = create((set) => ({
  kdramas: [
    {
      id: Math.floor(Math.random() * 100),
      name: "River Where the Moon Rises",
    },
    {
      id: Math.floor(Math.random() * 100),
      name: "The Crowned Clown",
    },
  ],
  addDrama: (payload) =>
    set(
      produce((draft) => {
        draft.kdramas.push({
          id: Math.floor(Math.random() * 100),
          name: payload,
        });
      })
    ),
  removeDrama: (payload) =>
    set(
      produce((draft) => {
        const dramaIndex = draft.kdramas.findIndex((el) => el.id === payload);
        draft.kdramas.splice(dramaIndex, 1);
      })
    ),
  patchDrama: (payload) =>
    set(
      produce((draft) => {
        const drama = draft.kdramas.find((el) => el.id === payload.id);
        // More logic goes here
      })
    ),
}));
Enter fullscreen mode Exit fullscreen mode

Now we just have to update the element's name property with the value of our payload.

// @src/store.js

import create from "zustand";
import produce from "immer";

export const useStore = create((set) => ({
  kdramas: [
    {
      id: Math.floor(Math.random() * 100),
      name: "River Where the Moon Rises",
    },
    {
      id: Math.floor(Math.random() * 100),
      name: "The Crowned Clown",
    },
  ],
  addDrama: (payload) =>
    set(
      produce((draft) => {
        draft.kdramas.push({
          id: Math.floor(Math.random() * 100),
          name: payload,
        });
      })
    ),
  removeDrama: (payload) =>
    set(
      produce((draft) => {
        const dramaIndex = draft.kdramas.findIndex((el) => el.id === payload);
        draft.kdramas.splice(dramaIndex, 1);
      })
    ),
  patchDrama: (payload) =>
    set(
      produce((draft) => {
        const drama = draft.kdramas.find((el) => el.id === payload.id);
        drama.name = payload.name;
      })
    ),
}));
Enter fullscreen mode Exit fullscreen mode

This way we can already update an element of the list, like this:

updating a korean drama

Conclusion

As you might have noticed, when using the immer, working with objects and arrays in our state makes the process much simpler, without having to worry about spread operations.

As always I hope this post has helped you, even if it was less informative and more hands on code.

Hope you have a great day! πŸ‘‹ ☺️

Top comments (7)

Collapse
 
seanmclem profile image
Seanmclem

This is cool. Using mutable state to make the zustand update functions easier

Collapse
 
franciscomendes10866 profile image
Francisco Mendes

So true. It's never been this easy. πŸ€“

Collapse
 
ivankleshnin profile image
Ivan Kleshnin

If you're going to use Immer with Zustand you should really consider Valtio. According to Zustand authors themself:

github.com/pmndrs/zustand/discussi...
github.com/pmndrs/zustand/discussi...
github.com/pmndrs/zustand/issues/483

Collapse
 
shkumbin profile image
Shkumbin Maksuti • Edited

Great article! You can also include immer as middleware in zustand.

Collapse
 
franciscomendes10866 profile image
Francisco Mendes

Thank you very much! Yes, but I decided to do it that way because with this approach you can replicate it in other state managers. 😊

Collapse
 
kishoreandra profile image
kishoreandra

beginner friendly , thanks mendes

Collapse
 
franciscomendes10866 profile image
Francisco Mendes

Thank you! I'm glad 😊