DEV Community

Cover image for An Introduction To Immer in React
Ashish Prajapati
Ashish Prajapati

Posted on

An Introduction To Immer in React

Introduction

When we deal with updating any nested or too deep property within an object (nested or flat level), it is possible that you might have ran into some weird issue where the state does not get properly updated or it completely destroys the application and we often find ourself doing these to fix it:-

// parsing from json
JSON.parse(JSON.stringify(BigNestedObject))

// lodash clone
lodash.clone(BigNestedObject, true)

// shallow copy, most people do not understand deep copy
const newObject = {...BigNestedObject}

// deep copy on every update. But very expensive
structuredClone(BigNestedObject)
Enter fullscreen mode Exit fullscreen mode

So to deal with all these sort of problems, we can use Immer and make our life much better once for all.


Table of content


What is Immer?

Immer is a developer friendly and performant package, which helps to manage immutable states in our application with ease. Read offical Docs here (https://immerjs.github.io/immer/)


What does Immer brings to the table?

  • Immer provides a way deal with Immutable data structures which allow for efficient change detection: if the reference to an object didn't change, the object itself did not change.
  • It makes cloning relatively cheap: Unchanged parts of a data tree don't get copied and are shared in memory with older versions of the same state.
  • We do not need to spread or copy the object again and again. No deep/shallow copy problem anymore.
  • We do not need to return anything from the functions provided by Immer. It's get automatic reflected and retured by the function.

Installation

npm i immer
Enter fullscreen mode Exit fullscreen mode

Simple Example with produce function of Immer

  • Managing simple list with produce function. It takes two argument, first one is initialState and second one is producer function where it recieves one argument draft a proxy of the initialState, which can mutated safely.
import {produce} from "immer"

const animeList= [
    {
        title: "One punch man",
        done: true
    },
    {
        title: "Hunter X Hunter",
        done: false
    }
]

// adding new item into the list
const nextState = produce(animeList, draftState => {
    draftState.push({title: "Hajime No Ippo"})
    draftState[1].done = true
})

console.log(nextState === animeList) // false
console.log(nextState[0] === animeList[0]) // true
console.log(nextState[1] === animeList[1]) // false

Enter fullscreen mode Exit fullscreen mode

Using Immer in React

  • We can use just produce function in our React app for updating/modifying the states, but we have more powerful tool provided by Immer, and it is useImmer hook. useImmer is just as same implementation like useState and it's syntax is same as to it, behind the scene it creates proxy and manages everything for us.

Installing use-immer library

npm i use-immer
Enter fullscreen mode Exit fullscreen mode

Below example is managing the animeList with immer. Where we are rendering the list, can add new item into the list, and can update the watch status of each item and rendering the whole state-tree in json.

import { useImmer } from 'use-immer';
import { useState } from 'react';

export default function AnimeList() {
  const [animeList, updateAnimeList] = useImmer([
    {
      id: 1,
      title: "One punch man",
      done: true
    },
    {
      id: 2,
      title: "Hunter X Hunter",
      done: false
    }
  ]);

  const [newAnime, setNewAnime] = useState('');

  // Toggle completion status
  const toggleAnime = (id) => {
    updateAnimeList(draft => {
      const anime = draft.find(item => item.id === id);
      if (anime) {
        anime.done = !anime.done;
      }
    });
  };

  // Add new anime
  const addAnime = () => {
    if (newAnime.trim()) {
      updateAnimeList(draft => {
        draft.push({
          id: Date.now(),
          title: newAnime.trim(),
          done: false
        });
      });
      setNewAnime('');
    }
  };

  // Mark all as watched/unwatched
  const toggleAll = () => {
    const allDone = animeList.every(anime => anime.done);
    updateAnimeList(draft => {
      draft.forEach(anime => {
        anime.done = !allDone;
      });
    });
  };

  const completedCount = animeList.filter(anime => anime.done).length;
  const totalCount = animeList.length;

  return (
    <div className="max-w-md mx-auto mt-8 p-6 bg-white rounded-lg shadow-lg">
      <h1 className="text-2xl font-bold text-gray-800 mb-6 text-center">
        🎌 My Anime List
      </h1>

      {/* Stats */}
      <div className="mb-4 p-3 bg-gray-50 rounded-lg">
        <p className="text-sm text-gray-600">
          Progress: {completedCount} of {totalCount} completed
        </p>
        <div className="w-full bg-gray-200 rounded-full h-2 mt-2">
          <div 
            className="bg-blue-500 h-2 rounded-full transition-all duration-300"
            style={{ width: `${totalCount ? (completedCount / totalCount) * 100 : 0}%` }}
          ></div>
        </div>
      </div>

      {/* Add new anime */}
      <div className="mb-6">
        <div className="flex gap-2">
          <input
            type="text"
            value={newAnime}
            onChange={(e) => setNewAnime(e.target.value)}
            placeholder="Add new anime..."
            className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
            onKeyPress={(e) => e.key === 'Enter' && addAnime()}
          />
          <button
            onClick={addAnime}
            className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
          >
            Add
          </button>
        </div>
      </div>

      {/* Toggle all button */}
      {totalCount > 0 && (
        <button
          onClick={toggleAll}
          className="w-full mb-4 px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors text-sm"
        >
          {animeList.every(anime => anime.done) ? 'Mark All as Unwatched' : 'Mark All as Watched'}
        </button>
      )}

      {/* Anime list */}
      <div className="space-y-3">
        {animeList.length === 0 ? (
          <p className="text-gray-500 text-center py-8">
            No anime in your list yet. Add some above! πŸ“Ί
          </p>
        ) : (
          animeList.map((anime) => (
            <div
              key={anime.id}
              className={`flex items-center justify-between p-3 rounded-lg border transition-all duration-200 ${
                anime.done 
                  ? 'bg-green-50 border-green-200' 
                  : 'bg-white border-gray-200 hover:border-gray-300'
              }`}
            >
              <div className="flex flex-row items-center gap-3">
                <button
                  onClick={() => toggleAnime(anime.id)}
                  className={`w-5 h-5 rounded-full border-2 flex items-center justify-center transition-all duration-200 ${
                    anime.done
                      ? 'bg-green-500 border-green-500 text-white'
                      : 'border-gray-300 hover:border-blue-500'
                  }`}
                >
                  {anime.done ? (
                    'completed' 
                  ) : 'unwatched'}
                </button>
                <span 
                  className={`transition-all duration-200 ${
                    anime.done 
                      ? 'text-green-700 line-through' 
                      : 'text-gray-800'
                  }`}
                >
                  {anime.title}
                </span>
              </div>
            </div>
          ))
        )}
      </div>

      {/* Debug info */}
      <div className="mt-6 p-3 bg-gray-50 rounded-lg">
        <h3 className="text-sm font-medium text-gray-700 mb-2">Current State:</h3>
        <pre className="text-xs text-gray-600 overflow-x-auto">
          {JSON.stringify(animeList, null, 2)}
        </pre>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

A brief comparison

// ❌ ❌ ❌ ❌ ❌  useState in nutshell
const [state, setState] = useState([])

setState((oldState) => {
  const clonedState = [...oldState]    // ❌ shallow cloning
  clonedState.push(someData)
  return clonedState   // ❌ returning cloned state
})


// βœ… βœ… βœ… βœ… βœ…   useImmer
const [state, setState] = useImmer([])

setState((oldState) => {
  clonedState.push(someData)   //βœ… just pushing new data
})


Enter fullscreen mode Exit fullscreen mode

Tips to better utilize Immer

  • Do not use immer where your state is not nested object or it's just flat level or dealing with only primitive values.
  • Using too much Immer in application can make it very memory heavy, as proxies takes up extra space.
  • We can use produce function in our reducers to manage the state.
  • Do not use it if your build size is constraint.
  • We can also use immer for managing Map object states. (https://immerjs.github.io/immer/map-set)

If learned something by reading the blog, please do like and share. πŸ˜„

Top comments (0)