Managing state in React apps is an important part of creating dynamic and responsive user interfaces. Traditionally, developers depended on immutable updates, which produce a new copy of the state with the necessary modifications. While this method provides predictability and facilitates debugging, it may have a performance penalty, particularly when dealing with big or complicated data structures.
This article covers two popular React frameworks for handling state; React Mutative and Immer. Both libraries promote immutability, albeit through distinct means.
Overview of React Mutative
Mutative's implementation is very comparable to that of Immer, although more robust. Mutative handles data more efficiently than Immer and native reducers. According to the Mutative team, this state management solution makes immutable updates easier. It is approximately two to six times faster than an average handcrafted reducer as well as more than 10 times faster than Immer.
Here are some benefits of Mutative.js:
- Conciseness: Code becomes more readable and easier to write.
- Performance: This can be faster than manual immutable updates, especially for large data structures.
- Error Reduction: Helps avoid accidental mutations of the state object.
For example, suppose there is a state object that contains a list. We intend to mark the final item on the list as done and then add a new item:
const state = {
list: [
{ text: 'Learn JavaScript', done: true },
{ text: 'Learn React', done: true },
{ text: 'Learn Redux', done: false },
],
};
If we were to use regular immutable data updates, we could write it as follows:
const nextState = {
...state,
list: [
...state.list.slice(0, 2),
{
...state.list[2],
done: true,
},
{ text: 'Learn Mutative', done: true },
],
};
But while using the Mutative, we can write it this way:
import { create } from 'mutative';
const nextState = create(state, (draft) => {
draft.list[2].done = true;
draft.list.push({ text: 'Learn Mutative', done: true });
});
This is the fundamental use of Mutative, which allows us to implement immutable updates more easily.
Overview of React Immer
Immer, which means always in German, is a little package that makes working with an immutable state more convenient. Winner of the 2019 JavaScript Open Source Award for "Most impactful contribution".
Benefits of React Immer
These benefits tend to be attained by ensuring you always create an edited copy of an object, array, or map rather than changing any of its properties. This can make writing code very difficult, and it's simple to break such restrictions unintentionally. Through the resolution of these issues, Immer will assist you in adhering to the immutable data approach. Here are some benefits of React Immer:
Immer will report an error if it detects an unintentional mutation.
Immer eliminates the requirement for the customary boilerplate code required when creating deep modifications to immutable objects. Without Immer, object duplicates must be made by hand at all levels. Typically, by doing a large number of... spread operations. When utilizing Immer, modifications are made to a draft object, which records them and creates the appropriate duplicates without changing the original object.
-
Immer does not require you to master certain APIs or data structures to profit from the paradigm. Immer allows you to access plain JavaScript data structures as well as the well-known mutable JavaScript APIs, all while remaining secure.
Code Examples
below is a simple example for a quick comparison:
const baseState = [ { title: "Learn JavaScript", done: true }, { title: "Try Immer", done: false } ]
Assume we have the previous base state, and we need to update the second todo and add a third one. However, we do not want to change the original baseState and avoid deep cloning (to maintain the first todo).
Without Immer
Without Immer, maintaining immutability requires us to copy every impacted level of the state object.
const nextState = baseState.slice() // shallow clone the array nextState[1] = { // replace element 1... ...nextState[1], // with a shallow clone of element 1 done: true // ...combined with the desired update } //Since nextState was freshly cloned, using push is safe here, // but doing the same thing at any arbitrary time in the future would // violate the immutability principles and introduce a bug! nextState.push({title: "Tweet about it"})
With Immer
Immer simplifies this process. We can use the generate function, which takes as its first argument the state we wish to start in, and as its second parameter a function called the recipe, which is handed a
draft
to which we can apply simple modifications. Once the recipe is complete, the mutations are recorded and used to create the next state. generate will handle all of the necessary copying while also protecting the data from future unintentional alterations by freezing it.import {produce} from "immer"
const nextState = produce(baseState, draft => {
draft[1].done = true
draft.push({title: "Tweet about it"})
})
Are you looking for an Immer with React? You are welcome to go directly to the [React + Immer](https://immerjs.github.io/immer/example-setstate) website.
## Core Functionality
Both Mutative and Immer achieve immutable updates in conceptually similar ways, but with key implementation differences that affect performance. Here's a breakdown:
1. **Draft State:** Both libraries create a draft version of the original state object. This draft behaves mostly like the original state, allowing you to write updates as if you were mutating it directly.
2. **Immutability Behind the Scenes:** When you make changes to the draft state, these libraries aren't modifying the original object. Instead, they track the changes you make.
3. **Generating a New State:** Once you're done with your updates in the draft state, Mutative, and Immer use different approaches to create a new, immutable state object reflecting the changes:
* **Mutative**: It leverages a high-performance algorithm to efficiently create a new data structure with the modifications from the draft. This approach prioritizes speed.
* **Immer**: It employs a proxy object that intercepts mutations on the draft and uses that information to construct a completely new, immutable state object. This approach offers more flexibility but can have some performance overhead.
## Performance Comparison
When it comes to raw performance, Mutative reigns supreme over Immer, especially for updates involving large data structures. Here's why:
* Speed Demon: Benchmarks show Mutative can be a staggering 2-6 times faster than writing state updates with traditional reducers and a whopping over 10 times faster than Immer in its default settings.
* Secret Sauce: This speed advantage boils down to how each library creates the final immutable state object. Mutative employs a highly optimized algorithm that efficiently builds a new data structure reflecting the changes you made in the draft, prioritizing raw speed.
* Immer's Overhead: Immer, on the other hand, utilizes a proxy object to track mutations on the draft state. This approach offers more flexibility but comes with an overhead that can significantly slow things down, particularly when dealing with large datasets. It's like adding an extra step in the process.
* Auto-Freeze and the Tradeoff: Immer offers an optional feature called auto-freeze, which helps prevent accidental modifications to the original state. This is great for safety, but it comes at a performance cost. Disabling auto-freeze can bring Immer closer to Mutative's speed, but then you lose the safety net.
Additionally, Immer enables automatic freezing by default as its optimal performance setting. Conversely, if you disable automatic freezing, the performance gap between Immer and Mutative could be as much as several dozen times. We have conducted extensive benchmark tests to confirm this point.
## The Ease of Use Comparison
Both Mutative and Immer offer an intuitive way to write state updates that appear to mutate the state but ultimately result in immutable updates. However, there are some differences in syntax and potential for errors:
**Syntax Mutative**: Leverages familiar syntax that resembles traditional mutations. You write code as if you're directly modifying the state object.
Example:
```jsx
const draft = createDraft(currentState);
draft.todos.push({ text: 'Buy milk' });
const newState = commit(draft);
Immer Syntax: Requires using specific Immer functions like produce to wrap your update logic.
Example:
const newState = produce(currentState, draft => {
draft.todos.push({ text: 'Buy milk' });
});
Draft Escapes
Draft escapes occur when you unintentionally modify the original state object instead of the draft copy in libraries like Mutative and Immer.
Mutative: While Mutative's syntax might feel familiar, there's a higher risk of accidentally modifying the original state object (draft escapes). This can happen if you forget to use the commit function to finalize the changes and create the new state. It requires stricter discipline to ensure all modifications go through the draft.
Immer: Immer's syntax explicitly forces you to work within the draft state using functions like produce. This reduces the chance of accidentally modifying the original state, making it inherently safer.
Conclusion
Auto-Freeze and the Tradeoff: Immer offers an optional feature called auto-freeze, which helps prevent accidental modifications to the original state. This is great for safety, but it comes at a performance cost. Disabling auto-freeze can bring Immer closer to Mutative's speed, but then you lose the safety net.
Top comments (1)
I am the author of Mutative. Thank you very much for your article.
There is one point that I feel is very necessary to clarify:
In fact, Mutative does not enable automatic freezing by default. However, we recommend considering enabling automatic freezing during development, as it can provide stricter immutable. It's worth noting that freezing does come with a performance cost.
Additionally, Immer enables automatic freezing by default as its optimal performance setting. Conversely, if you disable automatic freezing, the performance gap between Immer and Mutative could be as much as several dozen times. We have conducted extensive benchmark tests to confirm this point.
link: mutative.js.org/docs/getting-start...