iTranslated by AI
Immutable.js vs. Immer: Choosing the Right Library for Your Use Case
In modern frontend development, it has become mainstream to treat data as immutable objects. In other words, when data changes, instead of overwriting the object, a new object containing the new data is created. The main reason for this is that as objects are increasingly passed around various parts of a program as data, having the contents of data once handed over to someone else change later leads to confusion and makes system design difficult.
Creating a new object every time data changes can be tedious and make the program harder to read, especially when the data is complex or nested. This is where Immutable.js and Immer, libraries for handling data immutably, come into play.
While both libraries can achieve the goal of treating data as immutable, Immer currently sees more active development and seems to be more popular, partly because it allows you to work with plain JavaScript objects directly rather than using unique data structures.
However, the two libraries actually have different characteristics, and they need to be used appropriately. For common use cases, Immer is perfectly fine, but in specific scenarios, Immutable.js might be more suitable. The Japanese article Fighting Free-spirited JavaScript with the Expressive Power of TypeScript — Immutable.js Edition — briefly mentions this as follows (emphasis added by the author). This article will explain this in more depth.
Unlike Immutable.js, immer provides regular data structures provided by JavaScript by freezing them or wrapping them in proxies. Therefore, theoretically, it might be at a disadvantage in "scenarios where large collections are repeatedly modified," but in practice, it probably won't be an issue.
To summarize in one sentence: Immutable.js is a library that provides persistent data structures, whereas Immer does not. If you need to benefit from persistent data structures, choose Immutable.js. If you simply want to change JavaScript objects immutably, Immer is sufficient.
Example showing the difference between Immutable.js and Immer
First, let me show you an example that illustrates how Immutable.js and Immer differ. This example is quite favorable to Immutable.js, but it should be enough to understand its characteristics. The following example implements a process to create N arrays using both Immutable.js and Immer. For both, if N === 5, for example, they generate data like [[8],[8,1],[8,1,1],[8,1,1,4],[8,1,1,4,18]] (though the Immutable.js version is actually a List, not a JavaScript array). These arrays are generated by repeating the operation "adding one value to the previous array" rather than creating them from scratch each time.
function runImmutable(N: number) {
const result: List<number>[] = [];
let list = List<number>();
for (let i = 0; i < N; i++) {
list = list.push(rand());
result.push(list);
}
return result;
}
function runImmer(N: number) {
const result: number[][] = [];
let list: number[] = [];
for (let i = 0; i < N; i++) {
list = produce(list, (draft) => void draft.push(rand()));
result.push(list);
}
return result;
}
function rand() {
return Math.floor(Math.random() * 2 ** 5);
}
I compared the speeds of Immutable.js and Immer using the code above on my PC, and the results were as follows (if you'd like to try it yourself, you can do so from this CodeSandbox).
| N | Immutable.js | Immer |
|---|---|---|
| 1000 | 2.6 ms | 47.6 ms |
| 2000 | 8.6 ms | 152.5 ms |
| 3000 | 10.6 ms | 331.4 ms |
| 4000 | 4.0 ms | 584.1 ms |
| 5000 | 1.9 ms | 914.2 ms |
As you can see, Immutable.js is overwhelmingly faster for this task. Specifically, the time complexity for Immutable.js is actually produce is called, while Immutable.js shares objects within the internal structures of multiple Lists.
Immer creates a separate new array object as a result of each produce call, during which data is copied from the original array object to the new one. Thus, creating a single array object takes push is
Benefiting from this kind of computational complexity and memory usage is one of the primary purposes of using persistent data structures. Immutable.js is a library whose main goal is to provide such functionality, and its purpose differs from that of Immer.
As mentioned above, receiving these benefits in computational complexity and memory usage is one of the main purposes of using persistent data structures. Immutable.js is a library whose primary goal is to provide these functions, which makes its purpose different from Immer.
Note that while Immutable.js was overwhelmingly faster in the task of generating arrays, the performance when actually using those arrays might be different. This is because random access to a List in Immutable.js is
Frontend and Object Reuse
As described above, Immutable.js and Immer are libraries born from different objectives. Despite this, they are often compared because both share a common usage: representing state with immutable objects. For this purpose, Immer is more suitable, particularly because it allows data to be handled as plain objects rather than unique data structures like List.
The stagnation in the development of Immutable.js is likely due to the lack of demand for persistent data structures in the frontend. This type of data structure itself is a very important concept and exists in many programming languages. It is likely used extensively inside the browsers that we frontend engineers depend on for efficient data processing. However, what frontend engineers seek from immutability is not processing speed, but improved design. That is likely why Immer rose to prominence in place of Immutable.js.
In fact, even if you were to use neither Immutable.js nor Immer, you could update a single property of an object as follows:
const newObj = {
...obj,
foo: "bar",
};
If the size of obj (the number of properties) is
In other words, when performing such operations, we are not interested in reusing obj. While nested objects like obj.sub = { ... } are reused without being copied, that's about the extent of it; the data contained in obj itself is copied rather than reused.
If you are not interested in going that far, Immutable.js is unnecessary, and the more manageable Immer is sufficient. If you do need to go that far, you would use Immutable.js instead of Immer.
Summary
In this article, I have explained the differences between Immutable.js and Immer. While these libraries are sometimes used for the same purpose, they were actually created with very different goals in mind. Generally, Immer is fine, but the time may come when you need Immutable.js. I hope this article helps you prepare for such a scenario.
-
I haven't tracked the code, so it might be amortized complexity. ↩︎
Discussion