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)