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

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:

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

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

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

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

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

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

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

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

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

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

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

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! 👋 ☺️

55