What is Immer?

Immer is a Javascript library for immutable data that makes it easier to work with immutable data.

What is immutable data?

The concept of immutable data comes from functional programming. In functional programming, an initialized “variable” cannot be changed; a new “variable” is created each time it is changed.

Javascript does not implement immutable data at the language level and requires third-party libraries to do so. Immer is one such implementation (similarly, immutable.js).

Why immutable data?

ShouldComponentUpdate is introduced at length in the React performance optimization section, and immutable data is introduced from there. Using immutable data can solve the problems introduced by performance optimization, so this section is the background.

Performance optimization in React

Avoid Reconciliation

When a component’s props or state changes, React compares the latest returned element to the previously rendered element to determine whether it is necessary to update the actual DOM. React updates the DOM when they are different. Although React has guaranteed that unchanged elements will not be updated, even though React only updates changed DOM nodes, re-rendering still takes some time. For the most part it’s not a problem, but if it’s slow enough to be noticed, you can speed it up by overriding the lifecycle method shouldComponentUpdate. This method is triggered before rerendering. The default implementation always returns true to make React perform updates:

shouldComponentUpdate(nextProps, nextState) {
  return true;
}
Copy the code

If you know when your component doesn’t need to be updated, you can return false in shouldComponentUpdate to skip the entire rendering process. This includes the render call to the component and subsequent operations.

The role of shouldComponentUpdate

This is a subtree of a component. In each node, SCU represents the value returned by shouldComponentUpdate, and vDOMEq represents whether the React elements returned are the same. Finally, the color of the circle indicates whether the component needs to be reconciled.

shouldComponentUpdate
false
render
shouldComponentUpdate

For C1 and C3, shouldComponentUpdate returns true, so React needs to look down the child node. Here C6’s shouldComponentUpdate returns true, and React updates the DOM because Render returns a different element than before.

The last interesting example is C8. React calls render on this component, but since it returns the same React element as before, there is no need to update the DOM.

As you can see, React only changes the DOM of C6. For C8, real DOM rendering is skipped by comparing the React element to render. For children of C2 and C7, render is not called due to shouldComponentUpdate. So they don’t need contrast elements either.

The sample

In the previous section, React reconciled an interesting example, C8, which did not change at all. We can avoid such problems by using conditional judgment, avoiding mediation, and optimizing performance.

If your component needs to be updated only when the props. Color or state.count value changes, you can use shouldComponentUpdate to check:

class CounterButton extends React.Component {
  constructor(props) {
    super(props);
    this.state = {count: 1};
  }

  shouldComponentUpdate(nextProps, nextState) {
    if (this.props.color ! == nextProps.color) {return true;
    }
    if (this.state.count ! == nextState.count) {return true;
    }
    return false;
  }

  render() {
    return (
      <button
        color={this.props.color}
        onClick={()= > this.setState(state => ({count: state.count + 1}))}>
        Count: {this.state.count}
      </button>); }}Copy the code

In this code, shouldComponentUpdate only checks if props. Color or state.count has changed. If these values do not change, the component will not update. If your component is more complex, you can use a pattern like “shallow comparison” to check all the fields in props and state to determine if the component needs to be updated. React already provides a handy way to implement this common pattern – you just inherit the react. PureComponent (function components use react. Memo). So this code can be changed to a more concise form like this:

class CounterButton extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = {count: 1};
  }

  render() {
    return (
      <button
        color={this.props.color}
        onClick={()= > this.setState(state => ({count: state.count + 1}))}>
        Count: {this.state.count}
      </button>); }}Copy the code

But the react. PureComponent only makes shallow comparisons, so if the props or state are mutable in some way, shallow comparisons leave something out and you can’t use it. For example, arrays or objects are used:

class ListOfWords extends React.PureComponent {
  render() {
    return <div>{this.props.words.join(',')}</div>; }}class WordAdder extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      words: ['marklar']};this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    // This part of the code is bad and buggy
    const words = this.state.words;
    words.push('marklar');
    this.setState({words: words});
  }

  render() {
    return( <div> <button onClick={this.handleClick} /> <ListOfWords words={this.state.words} /> </div> ); }}Copy the code

The words array adds an element using the push method, but the reference to the words held by state does not change. Push directly changes the data itself without generating new data, and shallow comparators cannot perceive such changes. React generates incorrect behavior and will not re-render. Another problem is introduced for performance optimization.

The power of immutable data

The easiest way to avoid this problem is to avoid changing the value you are using for props or state. For example, the handleClick method above can be overridden with concat:

handleClick() {
  this.setState(state= > ({
    words: state.words.concat(['marklar'])})); }Copy the code

Or use the ES6 array extension operator:

handleClick() {
  this.setState(state= > ({
    words: [...state.words, 'marklar']})); };Copy the code

But when dealing with deeply nested objects, updating them immutable is confusing. For example, you might write code like this:

handleClick() {
  this.setState(state= > ({
    objA: {
      ...state.objA,
      objB: {
        ...state.objA.objB,
        objC: {
          ...state.objA.objB.objC,
          stringA: 'string',}},},}); };Copy the code

We need a more user-friendly library to help us intuitively use immutable data.

Why not use deep copy/compare?

Deep copy causes all components to receive new data, invalidating shouldComponentUpdate. Deep comparisons compare all values at a time, and when the data is deep and only one value changes, these comparisons are a waste of performance.

View layer code, we want it to be more responsive, so using the IMmutable library to manipulate immutable data is a space versus time trade-off.

Why Immer?

immutable.js

  • Maintains a set of data structures, Javascript data types andimmutable.jsTypes that require conversions to each other are intrusive to data.
  • The size of the library is relatively large (63KB), which is not suitable for mobile terminals with tight packet volume.
  • Apis are extremely rich and expensive to learn.
  • Compatibility is very good, support for older versions of IE.

immer

  • Proxy implementation, poor compatibility.
  • Small (12KB) and mobile-friendly.
  • The API is concise, uses Javascript’s own data types, and has almost no cost to understand.

In contrast, immer’s compatibility shortcomings are completely negligible in our environment. It’s much easier to use a library that doesn’t carry the burden of other concepts.

Immer overview

Immer is based on the copy-on-write mechanism.

The basic idea of Immer is that all changes are applied to the temporary draftState, which is a proxy for the currentState. Once all the changes are complete, Immer generates nextState based on the change in draft state. This means that you can interact with data by simply modifying it, while retaining all the benefits of immutable data.

This section focuses on the core API of Produce. Immer also provides some supporting apis, as described in the official documentation.

Core API: Produce

Grammar 1:

produce(currentState, recipe: (draftState) => void | draftState, ? PatchListener): nextState

Syntax 2:

produce(recipe: (draftState) => void | draftState, ? PatchListener)(currentState): nextState

The use of produce

import produce from "immer"

const baseState = [
    {
        todo: "Learn typescript".done: true
    },
    {
        todo: "Try immer".done: false}]const nextState = produce(baseState, draftState => {
    draftState.push({todo: "Tweet about it"})
    draftState[1].done = true
})
Copy the code

In the example above, changes to draftState are reflected on nextState and baseState is not modified. While the structure immer uses is shared, nextState shares unmodified portions structurally with currentState.

// the new item is only added to the next state,
// base state is unmodified
expect(baseState.length).toBe(2)
expect(nextState.length).toBe(3)

// same for the changed 'done' prop
expect(baseState[1].done).toBe(false)
expect(nextState[1].done).toBe(true)

// unchanged data is structurally shared
expect(nextState[0]).toBe(baseState[0])
// changed data not (dûh)
expect(nextState[1]).not.toBe(baseState[1])
Copy the code

Physical and chemical produce ke

The function passed to produce as the first parameter will undergo keratization. It returns a function that receives arguments that are passed to the function that receives produce. Example:

// mapper will be of signature (state, index) => state
const mapper = produce((draft, index) = > {
    draft.index = index
})

// example usage
console.dir([{}, {}, {}].map(mapper))
// [{index: 0}, {index: 1}, {index: 2}])
Copy the code

Reducer can be made good use of this mechanism:

import produce from "immer"

const byId = produce((draft, action) = > {
    switch (action.type) {
        case RECEIVE_PRODUCTS:
            action.products.forEach(product= > {
                draft[product.id] = product
            })
            return}})Copy the code

The return value of recipe

Normally, the recipe doesn’t need to return anything as shown, and draftState is automatically reflected in nextState as the return value. You can also return any data as nextState, provided draftState has not been modified.

const userReducer = produce((draft, action) = > {
    switch (action.type) {
        case "renameUser":
            // OK: we modify the current state
            draft.users[action.payload.id].name = action.payload.name
            return draft // same as just 'return'
        case "loadUsers":
            // OK: we return an entirely new state
            return action.payload
        case "adduser-1":
            // NOT OK: This doesn't do change the draft nor return a new state!
            // It doesn't modify the draft (it just redeclares it)
            // In fact, this just doesn't do anything at all
            draft = {users: [...draft.users, action.payload]}
            return
        case "adduser-2":
            // NOT OK: modifying draft *and* returning a new state
            draft.userCount += 1
            return {users: [...draft.users, action.payload]}
        case "adduser-3":
            // OK: returning a new state. But, unnecessary complex and expensive
            return {
                userCount: draft.userCount + 1.users: [...draft.users, action.payload]
            }
        case "adduser-4":
            // OK: the immer way
            draft.userCount += 1
            draft.users.push(action.payload)
            return}})Copy the code

Obviously, you cannot return undefined in this way.

produce({}, draft => {
    // don't do anything
})
Copy the code
produce({}, draft => {
    // Try to return undefined from the producer
    return undefined
})
Copy the code

Because in Javascript, not returning any value is the same as returning undefined, the function returns undefined. What if you want the immer to know that you really want to return undefined? Use immer’s built-in variable nothing:

import produce, {nothing} from "immer"

const state = {
    hello: "world"
}

produce(state, draft => {})
produce(state, draft => undefined)
// Both return the original state: { hello: "world"}

produce(state, draft => nothing)
// Produces a new state, 'undefined'
Copy the code

Auto freezing

Immer automatically freezes the state tree modified with Produce, which prevents modification of the state tree outside of the change function. This feature has a performance impact and needs to be turned off in production. You can use setAutoFreeze(true/false) to turn it on or off. This is recommended in development environments to avoid unpredictable state tree changes.

Use immer in setState

Using IMmer for deep status updates is simple:

/** * Classic React.setState with a deep merge */
onBirthDayClick1 = (a)= > {
    this.setState(prevState= > ({
        user: {
            ...prevState.user,
            age: prevState.user.age + 1}}}))/**
 * ...But, since setState accepts functions,
 * we can just create a curried producer and further simplify!
 */
onBirthDayClick2 = (a)= > {
    this.setState(
        produce(draft= > {
            draft.user.age += 1}}))Copy the code

Because Produce provides kerochemistry properties, you can directly pass the return value of produce to this.setState. Make the desired state changes within the recipe. Be intuitive and don’t introduce new concepts.

Hook immer

Immer also provides a React Hook library use-immer for using immers in hook mode.

useImmer

UseImmer is very similar to useState. It receives an initial state and returns an array. The first value of the array is the current state and the second value is the status update function. The status update function works just like the recipe in Produce.

import React from "react";
import { useImmer } from "use-immer";


function App() {
  const [person, updatePerson] = useImmer({
    name: "Michel".age: 33
  });

  function updateName(name) {
    updatePerson(draft= > {
      draft.name = name;
    });
  }

  function becomeOlder() {
    updatePerson(draft= > {
      draft.age++;
    });
  }

  return (
    <div className="App">
      <h1>
        Hello {person.name} ({person.age})
      </h1>
      <input
        onChange={e= > {
          updateName(e.target.value);
        }}
        value={person.name}
      />
      <br />
      <button onClick={becomeOlder}>Older</button>
    </div>
  );
}
Copy the code

Obviously, immer doesn’t work for this example :). This is just an example of how to use it.

useImmerReducer

Encapsulation of useReducer:

import React from "react";
import { useImmerReducer } from "use-immer";

const initialState = { count: 0 };

function reducer(draft, action) {
  switch (action.type) {
    case "reset":
      return initialState;
    case "increment":
      return void draft.count++;
    case "decrement":
      return voiddraft.count--; }}function Counter() {
  const [state, dispatch] = useImmerReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={()= > dispatch({ type: "reset" })}>Reset</button>
      <button onClick={()= > dispatch({ type: "increment" })}>+</button>
      <button onClick={()= > dispatch({ type: "decrement" })}>-</button>
    </>
  );
}
Copy the code

Refer to the article

  • Immer
  • use-immer
  • React – Optimizing Performance
  • Immutable Data with Immer and React setState
  • Immutability in React and Redux: The Complete Guide
  • Copy-on-write