Immer.js 101 — Making Redux state updates in React predictable

Jacek Szczerbiński
JavaScript in Plain English
6 min readFeb 10, 2021

--

Photo by Altum on Unsplash

Why Redux? Why Immer?

You either hate or love Redux. I’m definitely a person who loves Redux but let’s be honest — using it in every project is unnecessary. If you’re just making a prototype or a small app — it’s better to use hooks and state.

Although, from my experience, every product which becomes commercial — grows. And new features mean a bigger state to manage. It’s better to design app architecture having in mind that you will eventually have a big state. For example, our main React app has more than 90k lines of code, uses API calls, WebSockets and state has arrays containing hundreds of objects. I cannot even imagine how to manage this without Redux.

That’s having said — Redux might be dangerous if used by an inexperienced developer. One of the biggest concerns is to manage state immutability in Redux. But an even more complex issue is causing unnecessary re-rendering of components with mistakes in reducers. And a simple solution to this is Immer. Let’s dig into problems with immutability/re-renderings, and then I will show you my personal approach to how I utilize Immer.

Common immutability issues in Redux

Let’s focus on reducers since this should be the only place where the state changes. Reducers are pretty straightforward — don’t mutate state, return a new state.

Mutating state directly

This one is obvious. Do not make any operations on the state, so for example:

Don’t do this!

Because we are mutating the original state and then returning it — Redux won’t recognize any changes. Hence, components connected to this part of store won’t update. Redux compares new state with old state (first argument in reducer function). So, the reducer function must be always a PURE FUNCTION. To avoid any similar bug you should also always use pure functions inside reducer functions. Below is an example of pure function.

Just to be sure we are on the same page with pure functions

Unnecessary re-rendering

This one is not so obvious, and I see many times this mistake. I have seen it in countless React tutorials. In bad ones and in top-notch ones as well! Let’s look at the below example:

This is really a common mistake!

Now, just to be clear — using filter function is perfectly fine. It creates and returns a copy of state. But the copy of state is causing an issue. Redux does every time shallow comparison. And in shallow comparison those are different objects since the filter is making copy. Same goes for map, reduce and so on.

Why shallow comparison then? Well, Redux team decided that doing deep comparison every time will be pretty demanding when it comes to resources. It is logical, and I have no issue with that.

Moreover, — if your application is small, this might not be a big issue (maybe that’s why tutorials have this mistake? What do you think?) But in applications with hundreds of components connected to Redux and API calls which are updating state — this becomes serious. With big scale application, you cannot afford useless re-rendering. It is especially frustrating in React Native apps and can bring down performance drastically.

Now, to address this issue we can use some simple refactoring:

Some defensive programming in action

Nothing extreme, right? With some discipline and defensive programming we ensure that state updates only when necessary. Now this is good time to introduce Immer.

Here comes Immer!

Immer is already a well-established library. Almost 6 million downloads weekly (according to npm — January 2021), 100% code coverage, +100 contributors, 19k GitHub stars. Those numbers are impressive. Even more impressive is the fact that Immer was named 2019 “Most impactful contribution” in JavaScript open source and “Breakthrough of the year” React open-source award.

Is Immer really a breakthrough for React?

Don’t get me wrong — I love Immer. However, it is not a magic library that will solve all problems effortlessly. It requires following some patterns — and if used unmindfully won’t save you from unnecessary state update. You can surely achieve the same results on your own without additional library. Nevertheless, Immer promotes best practices, clean code and reduces code repetition. In my opinion, it is a must in any React+Redux app.

How Immer works

It’s surprisingly simple. The Main function in Immer is produce. It has two arguments, state and callback function which has draft(of state). Within the callback function we make changes directly on draft. Produce then compares differences and generates nextState.

https://immerjs.github.io/immer/img/immer.png

Let’s see how Immer is used in reducers:

Focus now on myReducer function. We use here concept called currying. It is not obligatory, there are alternative ways of using Immer — but I prefer this one. It shows clearly what is happening. We extend a reducer with produce function. In produce, we use callback function to make operations directly on draft. I like to limit my reducers to simple switch-case usage. In every case I then return the function which does the necessary job on draft. What is important — we do not have to return draft! Therefore, I do not use default case — although adding:

default: return draft; / return;

It’s perfectly fine — but unnecessary. I will go through most scenarios at the end of the article.

Bonus: Adjusting store for Immer

Before going deep into practical scenario — let me share my experiences with Immer. I believe that Immer works best with normalized state or simply maps. Using Immer on an array of complicated API objects is possible, but sometimes frustrating. Especially with nested arrays of objects. Normalized state solves many issues which we faced in production quality React apps (duplication, easy access and update of objects etc.). Nevertheless, this is not obligatory, and I will cover both arrays and objects(maps).

Most common scenarios in reducers with Immer

Immer with normalized state / objects

Immer with normalized state

The Code is rather self-explanatory. Methods are clean and easy to understand. I like keeping reducer as small as possible.

updateObject — In his method we are a bit lazy, we do not check if the object has really changed — we overwrite it. Normalized state has one significant advantage, access/write/delete has Big O(1). It’s up to you if you need some utils to check if the object is different.

updateObjectStatus — Maybe hasObject is unnecessary, but I’ve added this to make it easy to understand. First we check if this object is already in state. If so, we update status property. The Cool thing is that Immer will compare draft with status — and if status will be the same (let’s say ‘done’) — it won’t update the state.

deleteObject — This one is obvious :) delete is so useful that we do not need to check if state has this id.

case AT.RESET_OBJECT — I’ve mentioned that we do not need to return draft, produce function doesn’t need it. But what if we want to reset or make state empty? We then return the new draft. In this case initial State.

replaceWithNewObjects — Now let’s assume we receive from API completely new state. Maybe few items will be the same, but we have decided that, with this particular action we will replace the state. In this case, using my utils function we create a new state (with accordance to normalized state) and return it. Since we do not make any operations on draft — I pass to replaceWithNewObjects function only payload.

Immer with arrays

Immer with array of objects

At first glance we can see advantages of normalized state vs. array of objects. We need almost every time make operations with Big O(n) to check if the object is present in state. I think that code is clear and don’t need additional explanation. :) If you think otherwise, have some questions — please leave comment! I will update the article.

Wrapping up

Using Immer might be at first counterintuitive — since we mutate draft. From my experience using Immer with normalized state gives best results when it comes to clean code, readability and having control over state.

Can we have the same effects without Immer? Sure! It’s up to you. Still, it is another library and dependency added to the project.

--

--