Why use Immer for state updates?

Just a guy who loves to write code and watch anime.
Search for a command to run...

Just a guy who loves to write code and watch anime.
No comments yet. Be the first to comment.
Qualities that make outstanding builders.

Bipedal vs quadrupedal: how many legs This is about the number of legs the creature walks on. Bipedal. Two legs. Humans, ostriches, T-rex, kangaroos, most fantasy humanoids. Quadrupedal. Four legs. Do

Intro You hear "lerp" and "smoothstep" everywhere in game dev. They sound like math jargon. They're not. Both are small tools that do the same job: smoothly move from one value to another. The problem

What is Kinematics Kinematics is the math field. It's the study of how things move without worrying about forces (which would be dynamics). FK and IK are the two branches: forward kinematics and inver

Intro Textures are usually the biggest cost in a 3D scene. Memory, bandwidth, and load time all get eaten by them. Resizing them is the obvious lever. There's more. This post is about the less obvious

In React (and modern JavaScript in general), we aim for immutable state updates.
Why? Because comparing objects by reference is MUCH faster than doing a deep comparison. If an object's reference hasn't changed, we can be certain that nothing inside it has changed.
In React, imagine you have a component that receives a large object as a prop:
const UserDashboard = ({ userData }) => {
// userData might be:
// {
// profile: { ... },
// posts: [ ... ],
// friends: [ ... ],
// settings: { ... }
// }
}
React needs to decide if it should re-render this component when the parent updates.
It has two options:
Deep comparison: Look at every single property in userData, recursively checking if any value changed. This is expensive for large objects! 😬
Reference comparison: Just check if userData === prevUserData. This is just one operation. Much faster!
// Deep comparison (slow)
function isEqual(obj1, obj2) {
for (let key in obj1) {
if (typeof obj1[key] === 'object') {
if (!isEqual(obj1[key], obj2[key])) return false
} else if (obj1[key] !== obj2[key]) {
return false
}
}
return true
}
// Reference comparison (fast!)
userData === prevUserData // single operation
This is why React uses reference equality by default (like in useMemo, useEffect dependencies, React.memo, etc).
The catch: For this to work reliably, we need to ensure that if we change ANY part of an object, we get a new reference.
If we mutate directly:
const userData = { name: "John", age: 30 }
userData.age = 31 // Same reference! React won't know it changed 🔴
const userData = { name: "John", age: 30 }
const newUserData = { ...userData, age: 31 } // New reference! React can detect this
Here's the pain point:
const state = {
users: [
{ id: 1, name: "John" },
{ id: 2, name: "Mary" }
],
settings: {
theme: "dark",
notifications: {
email: true,
push: false
}
}
}
// Want to update Mary's name and enable push notifications?
const newState = {
...state,
users: state.users.map(user =>
user.id === 2 ? { ...user, name: "Mary Jane" } : user
),
settings: {
...state.settings,
notifications: {
...state.settings.notifications,
push: true
}
}
}
Look at all that spreading!
This is:
Prone to mistakes (easy to miss a spread)
Difficult to see what changes
Becomes more complicated with deeper state
Immer's smart idea: What if we could write code in the usual way but still get updates that don't change the original data?
const newState = produce(state, draft => {
draft.users[1].name = "Mary Jane"
draft.settings.notifications.push = true
})
This is:
Intuitive → write code as you normally would ✨
Safe → you can't accidentally change the original ✨
Clear → you can easily see what's changing ✨
Efficient → Immer only copies the parts that actually changed ✨
The magic of Immer is that it creates a proxy of your state (the draft), tracks the changes you make, and only creates new references for the parts that have changed. Everything else keeps the same reference for better performance.