Immer – Create the next immutable state by mutating the current one

Immer is a way to create immutable objects by mutate the current object. For example, when we use React, we often encounter the problem of not updating memo or unnecessary updates. Immer can be used to solve the problem

const Profile = memo((props) = > {
  console.log("render <Profile />");
  return (
    <>
      <div>Name: {props.user.name}</div>
      <div>Age: {props.user.age}</div>
    </>
  );
});

const Blog = memo((props) = > {
  console.log("render <Blog />");
  return <div>blog: {props.blog.content}</div>;
});

class User extends Component {
  state = {
    user: {
      name: "Michel".age: 33
    },
    blog: {
      content: "hahah..."}};render() {
    return (
      <div>
        <Profile user={this.state.user} />
        <Blog blog={this.state.blog} />
        <button onClick={this.onUpdateAgeByManual0}>onUpdateAgeByManual0</button>
        <button onClick={this.onUpdateAgeByManual1}>onUpdateAgeByManual1</button>
        <button onClick={this.onUpdateAgeByManual2}>onUpdateAgeByManual2</button>
        <button onClick={this.onUpdateAgeByImmer}>onUpdateAgeByImmer</button>
      </div>
    );
  }

  onUpdateAgeByManual0 = () = > {
    this.setState((prevState) = > {
      prevState.user.age++;
      return {
        user: { ...prevState.user },
        blog: prevState.blog
      };
    });
  };

  onUpdateAgeByManual1 = () = > {
    this.setState((prevState) = > {
      prevState.user.age++;
      return { ...prevState };
    });
  };

  onUpdateAgeByManual2 = () = > {
    this.setState((prevState) = > {
      prevState.user.age++;
      return {
        user: { ...prevState.user },
        blog: { ...prevState.blog }
      };
    });
  };

  onUpdateAgeByImmer = () = > {
    this.setState(
      produce(this.state, (draft) = > {
        draft.user.age += 1; })); }; }Copy the code

Can you guess how the onUpdate EByManual will be updated?

Click me to show the answer…
  • OnUpdateAgeByManual0: Ideal update status that updates only Profile components
  • OnUpdateAgeByManual1: There will be no component updates, the reference to the User object will not change, and the memo will not be updated after a shallow comparison
  • OnUpdateAgeByManual2: causes unnecessary component updates, changes in references to the blog object, and blog component updates triggered after a shallow memo comparison
  • OnUpdateAgeByImmer: with onUpdateAgeByManual0

So what immutable guarantees is that a new object is created every time it is updated. Deep copy is fine, but for performance reasons, you need to make sure that only the properties of the object that have changed generate new references, and the other properties that have not changed still use the old references, which is what Immer does

The principle of

In the words of the author:

  1. Copy on write
  2. Proxies

The work of Produce is divided into three stages, namely create proxy (createDraft), modify proxy (produceDraft), finalize (Finalize). What create proxy does is to proxy the first parameter base object passed in. That is, when the callback is passed in, it can perform the ShallowCopy on write operation, which ultimately points a reference to the modified object to the ShallowCopy object

So the key principle is how do you implement ShallowCopy on Write and how do you proxy

ShallowCopy on write

ShallowCopy on Write works like the one shown above. Here’s an example to understand the process

const state = {
  user: {
    name: "Michel".age: 33
  },
  blog: {
    content: "hahah..."}};const nextState = produce(state, (draft) = > {
  draft.user.age = draft.user.age + 1;
});
Copy the code

An object is structured like a tree, with properties of the primitive type acting as leaf nodes

First of all, when reading, that is, when the get operation of the object is triggered, the value will be returned according to the type of the value. If the value is a basic type, it means that the leaf node is accessed and can be returned directly. If it is a reference type, continue to create a proxy for that value, implementing a “lazy proxy.” In the draft.user.age + 1 example, the user on draft is read first, and the proxy of user is returned. Then the age property on user is read, and the basic type 33 is returned

After the agent is created for the first time, it is stored. In this way, when the agent is read again, it can directly return the stored agent, reducing the overhead of creating the agent and storing the operation on the agent. In this way, the operation on the agent will not be invalid when the agent is created every time. In this example, the part before the draft.user.age = set operation is equivalent to reading again to get the previous agent

Then, when modified, a shallow copy is created, and the parent node makes a shallow copy and changes on the shallow copy objects, while Object.assign(state.copy, state.drafts) re-stores the previous agent on the shallow copy objects, again ensuring that the previous actions are valid. In this example, the draft.user.age = set operation triggers the set operation of the user agent, creates a shallow copy of user, then creates a shallow copy of Draft, and modifies the age attribute of the user shallow copy object

When finalize, the object tree is traversed. If the proxy is changed, the shallow copy object is returned. If no changes are made, only the original object is returned

Proxies

Immer uses the Proxy API to delegate objects and arrays. Map and Set are created by DraftMap and DraftSet overrides Set, GET, add, delete, and has. The ES5 environment uses defineProperty to Proxy objects and arrays, so the Proxy API is not necessary, just to facilitate the operation of the Proxy object, we can also specify some methods to implement the Proxy operation

implementation

The implementation of about 100 lines can be seen in AHABHgK/simple-IMmer. Immer V1.0.0 has made some simplification, the principle is clearer, and supports Corrification and asynchronization. There will be annotations for difficult points, as well as relatively complete tests, so it is easy to get started and debug

In addition, since Immer supports Currization, it is easy to implement useImmer, which can be viewed directly on Github

Plan to change the writing style, before the three Vue3 articles have large sections of paste code, smelly and long, will only hypnotize it, later to write will try to explain the principle, write concise, in addition to very refined will paste code, other times the source directly to the link, there are difficult points will be written through annotations and tests