Open source on Github, welcome to Fork, Star!
preface
Immer is an IMmutable library written by the author of Mobx. The core implementation is to use ES6 proxy to implement JS immutable data structure with minimal cost. It is easy to use, small size, ingenious design, and meets our needs for JS immutable data structure. Unfortunately, there are too few perfect documents on the network, so I wrote one. This article gives a comprehensive explanation of Immer with the ideas and processes close to actual combat.
Problems with data processing
We will define an initial object for later examples: we will define a currentState object, which we will refer to when we use the variable currentState, unless otherwise specified
let currentState = {
p: {
x: [2],
![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/6/8/17293174bf805c43~tplv-t2oaga2asx-image.image)}},Copy the code
What happens when you accidentally modify the original object?
// Q1
let o1 = currentState;
o1.p = 1; // currentState has been modified
o1.p.x = 1; // currentState has been modified
// Q2
fn(currentState); // currentState has been modified
function fn(o) {
o.p1 = 1;
return o;
};
// Q3
leto3 = { ... currentState }; o3.p.x =1; // currentState has been modified
// Q4
let o4 = currentState;
o4.p.x.push(1); // currentState has been modified
Copy the code
A way to resolve the modification of reference type objects
- Deep copy, but the cost of deep copy is high, affecting performance.
- ImmutableJS is a great library for immutable data structures, But ImmutableJS has two major disadvantages compared to Immer:
- It requires users to learn how to operate its data structure, which is not as simple and easy to use as using native objects provided by Immer.
- The result of its operation needs to pass
toJS
Method to get the native object, which makes it necessary to always be aware of whether you are operating on the native object or the return result of ImmutableJS when operating on an object. If you are not careful, unexpected bugs can occur.
We’re not happy with any of the solutions we know so far, so what’s so brilliant about Immer?
Immer function introduction
Install immer
If you want to do good things, you need to do good things first, and installing Immer is the first priority right now
npm i --save immer
Copy the code
How does Immer fix uncomfortable problems
Fix Q1 and Q3
import produce from 'immer';
let o1 = produce(currentState, draftState= > {
draftState.p.x = 1;
})
Copy the code
Fix Q2
import produce from 'immer';
fn(currentState);
function fn(o) {
return produce(o, draftState= > {
draftState.p1 = 1; })};Copy the code
Fix Q4
import produce from 'immer';
let o4 = produce(currentState, draftState= > {
draftState.p.x.push(1);
})
Copy the code
Is it very simple to use, through a small test, we have a simple understanding of Immer, the following will be introduced to the common API of Immer.
The concept that
There are not many concepts involved in Immer, and the concepts involved are listed here first. If you don’t understand the concepts in the process of reading this article, you can always come here for reference.
-
CurrentState Initial state of the object being manipulated
-
DraftState is the draftState generated based on currentState, which is a proxy for currentState, and any changes made to draftState will be recorded and used to generate nextState. CurrentState will not be affected during this process
-
NextState Final state generated according to draftState
-
Produce is used to generate functions for nextState or producer
-
Producer produce is generated to produce nextState and performs the same operation each time
-
The recipe production machine is used to operate the draftState function
Common apis
Before using Immer, be sure to introduce the Immer package into the module
import produce from 'immer'
Copy the code
or
import { produce } from 'immer'
Copy the code
Produce is exactly the same for both types of citation
produce
Note: AppearPatchListener
I’ll skip this and cover it in a later chapter
The first way of use:
Grammar: produce (currentState, recipe: (draftState) = > void | draftState,? PatchListener): nextState
Example 1:
let nextState = produce(currentState, (draftState) = > {
})
currentState === nextState; // true
Copy the code
Example 2:
let currentState = {
a: [].p: {
x: 1}}let nextState = produce(currentState, (draftState) = > {
draftState.a.push(2);
})
currentState === nextState // false
currentState.a === nextState.a; // false
currentState.p === nextState.p; // true
Copy the code
Thus, any changes to draftState are reflected in nextState. NextState shares unmodified portions with currentState structurally, and Immer uses a shared structure. The sharing effect is as follows:
Automatic freezing function
One neat thing Immer does internally is that the nextState generated by produce is frozen. Freeze only what nextState changed compared to currentState), so that an error will be reported when you modify nextState directly. This makes nextState truly immutable data.
Example:
const currentState = {
p: {
x: [2],}};const nextState = produce(currentState, draftState= > {
draftState.p.x.push(3);
});
console.log(nextState.p.x); / / [2, 3]
nextState.p.x = 4;
console.log(nextState.p.x); / / [2, 3]
nextState.p.x.push(5); / / an error
Copy the code
The second way of use
Using the characteristics of higher-order functions, a producer is generated
Grammar: produce (recipe: (draftState) = > void | draftState,? PatchListener)(currentState): nextState
Example:
let producer = produce((draftState) = > {
draftState.x = 2
});
let nextState = producer(currentState);
Copy the code
The return value of recipe
If recipe does not return a value, nextState is generated according to draftState. NextState is generated from the return of the recipe function.
let nextState = produce(currentState, (draftState) = > {
return {
x: 5}})console.log(nextState); // {x: 5}
Copy the code
At this point, nextState is no longer generated from draftState, but from the return value of recipe.
The recipe of this
This inside recipe points to draftState, so modifying this has the same effect as modifying the recipe parameter draftState. Note: the recipe function here cannot be an arrow function, if it is, this cannot point to draftState
produce(currentState, function(draftState){
// Here, this points to draftState
draftState === this; // true
})
Copy the code
Patch Patch function
With this feature, detailed code debugging and tracking is easy, every change to draftState is known, and time travel is possible.
In Immer, a patch object is as follows:
interface Patch {
op: "replace" | "remove" | "add" // The action type of a change
path: (string | number) []// This property refers to the path from the root to the changed branchvalue? :any // This attribute is available only when op is replace or add, indicating a new assignment
}
Copy the code
Grammar:
produce(
currentState,
recipe,
// Use the patchListener function to expose the forward and reverse patch arrays
patchListener: (patches: Patch[], inversePatches: Patch[]) = > void
)
applyPatches(currentState, changes: (patches | inversePatches)[]): nextState
Copy the code
Example:
import produce, { applyPatches } from "immer"
let state = {
x: 1
}
let replaces = [];
let inverseReplaces = [];
state = produce(
state,
draftState= > {
draftState.x = 2;
draftState.y = 2;
},
(patches, inversePatches) = > {
replaces = patches.filter(patch= > patch.op === 'replace');
inverseReplaces = inversePatches.filter(patch= > patch.op === 'replace');
}
)
state = produce(state, draftState= > {
draftState.x = 3;
})
console.log('state1', state); // { x: 3, y: 2 }
state = applyPatches(state, replaces);
console.log('state2', state); // { x: 2, y: 2 }
state = produce(state, draftState= > {
draftState.x = 4;
})
console.log('state3', state); // { x: 4, y: 2 }
state = applyPatches(state, inverseReplaces);
console.log('state4', state); // { x: 1, y: 2 }
Copy the code
The value of state. X is printed for 4 times, and the results are as follows: 3, 2, 4, and 1, respectively. Patches and inversePatches can be printed respectively.
Patches data are as follows:
[{op: "replace".path: ["x"].value: 2
},
{
op: "add".path: ["y"].value: 2},]Copy the code
InversePatches data are as follows:
[{op: "replace".path: ["x"].value: 1
},
{
op: "remove".path: ["y"],},]Copy the code
It can be seen that data operation is recorded internally in patchListener and stored as forward operation record and reverse operation record respectively for our use.
This concludes our overview of Immer’s common functions and apis.
Next, we will look at how Immer can be used to improve the efficiency of React and Redux projects.
Optimize react project exploration with immer
Start by defining a state object, which later examples refer to when using the variable state or when accessing this.state without special declarations
state = {
members: [{name: 'ronffy'.age: 30}}]Copy the code
Throw a demand
For the state defined above, let’s throw out a requirement to keep the rest of the discussion focused: The first member of the members of the group increases in age by one year
Optimize the setState method
The wrong sample
this.state.members[0].age++;
Copy the code
However, some novice students will make such mistakes, mainly because it is too convenient to operate in this way, so that they forget the rules of operating state.
Let’s look at the correct implementation
The first implementation of setState
const { members } = this.state;
this.setState({
members: [
{
...members[0].age: members[0].age + 1,},... members.slice(1)]})Copy the code
The second implementation of setState
this.setState(state= > {
const { members } = state;
return {
members: [
{
...members[0].age: members[0].age + 1,},... members.slice(1)]}})Copy the code
The above two implementation methods are the two use methods of setState, which must be familiar to everyone. Now let’s see, if we use Immer, what kind of fireworks do we get?
Update state with immer
this.setState(produce(draftState= > {
draftState.members[0].age++;
}))
Copy the code
Is it immediately much less code and easier to read?
Optimization of reducer
The produce of immer is an extended use
Before we begin our formal exploration, let’s take a look at the expanded use of produce in the second way:
Example:
let obj = {};
let producer = produce((draftState, arg) = > {
obj === arg; // true
});
let nextState = producer(currentState, obj);
Copy the code
Compared with the example of producing, an obJ object is defined and passed in as the second parameter of the producer method. As you can see, the second argument to the recipe callback in Produce points to the same block of memory as the obj object. Ok, now that we know this extended use of produce, let’s see how it works in Redux.
How do normal Reducer resolve the requirements presented above
const reducer = (state, action) = > {
switch (action.type) {
case 'ADD_AGE':
const { members } = state;
return {
...state,
members: [
{
...members[0].age: members[0].age + 1,},... members.slice(1)]}default:
return state
}
}
Copy the code
Reducer reducer set immer
const reducer = (state, action) = > produce(state, draftState= > {
switch (action.type) {
case 'ADD_AGE':
draftState.members[0].age++; }})Copy the code
As you can see, with Produce, we’ve streamlined our code a lot; However, a closer look shows that the code can be more elegant by taking advantage of the fact that Produce can produce producer:
const reducer = produce((draftState, action) = > {
switch (action.type) {
case 'ADD_AGE':
draftState.members[0].age++; }})Copy the code
Ok, so far, the reducer optimization method of Immer has been explained.
The use of Immer is very flexible, and there are other extension apis, so do some research and you’ll find many more great uses for Immer!
Reference documentation
- The official documentation
- Introducing Immer: Immutability the easy way