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)
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?
- What does Immer brings to the table?
- Installation
- Simple Example with Immer produce function
- Using Immer in React
- Tips to better utilize Immer
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
Simple Example with produce function of Immer
- Managing simple list with
produce
function. It takes two argument, first one isinitialState
and second one isproducer
function where it recieves one argumentdraft
a proxy of theinitialState
, 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
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 isuseImmer
hook.useImmer
is just as same implementation likeuseState
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
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> ); }
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 })
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)