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 run into some weird issues:-
- State does not get properly updated.
- Updating it affects somewhere else.
- Or it completely destroys the application.
- Always creating shallow copy and updating the state in immutable way.
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. Let's understand more about it.
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. Updating the state in mutable way but it's immutable. 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
producefunction. It takes two argument, first one isinitialStateand second one isproducerfunction where it recieves one argumentdrafta 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
producefunction in our React app for updating/modifying the states, but we have more powerful tool provided by Immer, and it isuseImmerhook.useImmeris just as same implementation likeuseStateand 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 [list, updateList] = useImmer([
{ id: 1, title: "One Punch Man", done: true },
{ id: 2, title: "Hunter X Hunter", done: false }
]);
const [input, setInput] = useState('');
const toggle = (id) => {
updateList(draft => {
const item = draft.find(a => a.id === id);
if (item) item.done = !item.done;
});
};
const add = () => {
if (input.trim()) {
updateList(draft => {
draft.push({ id: Date.now(), title: input.trim(), done: false });
});
setInput('');
}
};
return (
<div className="max-w-md mx-auto mt-8 p-6 bg-white rounded shadow">
<h1 className="text-xl font-bold mb-4">Anime List</h1>
<div className="flex gap-2 mb-4">
<input
value={input}
onChange={e => setInput(e.target.value)}
onKeyPress={e => e.key === 'Enter' && add()}
placeholder="Add anime..."
className="flex-1 px-3 py-2 border rounded"
/>
<button onClick={add} className="px-4 py-2 bg-blue-500 text-white rounded">
Add
</button>
</div>
<div className="space-y-2">
{list.map(anime => (
<div key={anime.id} className="flex items-center gap-2 p-2 border rounded">
<input
type="checkbox"
checked={anime.done}
onChange={() => toggle(anime.id)}
/>
<span className={anime.done ? 'line-through text-gray-400' : ''}>
{anime.title}
</span>
</div>
))}
</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
producefunction 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)