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

  1. Deep copy, but the cost of deep copy is high, affecting performance.
  2. 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 passtoJSMethod 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: AppearPatchListenerI’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