I have heard the name of ImmerJS for a long time. I have nothing to do at work today

The current version of ImmerJS is 9.0.6 (27 October 2021)

$Yarn add [email protected]
Copy the code

ImmerJS uses proxy to implement immutable data types. Compared to ImmutableJs, there is no need to implement a set of data structures internally, so there are many fewer concepts, APIS and volumes (ImmutableJs is huge, around 60KB).

1 Getting Started

1.1 contrast

Now prepare a base state

const baseState = [
    {
        title: "Learn TypeScript".done: true
    },
    {
        title: "Try Immer".done: false}]Copy the code

Now, we want to change the second baseState entry done to true, add a new entry, and store the value in the new nextState (to avoid baseState’s value changing)

Now let’s look at the following three scenarios

1.1.1 native js

/ / native js
const nextState = [...baseState]
nextState[1] = {
  ...nextState[1].done: true
}
nextState.push({
  title: 'React18 is coming'.done: false
})

console.log(JSON.stringify(baseState, null.2))
console.log(JSON.stringify(nextState, null.2))
Copy the code

The result is two trees with different data structures

1.1.2 ImmutableJs

Let’s install immutableJs

$Yarn add [email protected]
Copy the code

Use the following

import { List } from 'immutable'

// Convert the array to an immutableJs internal structure
const _baseState = List(baseState)
// immutableJs returns a new object every time it changes
const array2 = _baseState.push({
  title: 'React18 is coming'.done: false
})
const nextState = array2.update(1.oldValue= >{
  // In essence, the structure is still native
  return {
    ...oldValue,
    done: true}})console.log(JSON.stringify(_baseState.toArray(), null.2))
console.log(JSON.stringify(nextState.toArray(), null.2))
Copy the code

1.1.3 ImmerJs

import produce from 'immer'

const nextState = produce(baseState, draft= > {
    draft[1].done = true
    draft.push({ title: 'React18 is coming'.done: false})})console.log(JSON.stringify(baseState, null.2))
console.log(JSON.stringify(nextState, null.2))
Copy the code

ImmerJs also does not change the address pointing without modifying the data

console.log(nextState[0] === baseState[0])   // true
Copy the code

1.2 ImmerJs extension

ImmerJs is only 1/8 the size of ImmutableJs, but ImmerJs can only monitor arrays and objects. For maps and sets ImmerJs cannot monitor, or if you want to be compatible with ES5, you can turn on the corresponding functions

import produce, { enableES5, enableMapSet, enablePatches, enableAllPlugins } from 'immer'

enableES5()
enableMapSet()
enablePatches()
enableAllPlugins()
Copy the code
New and old technical support use volume
produce 7.83 KB
Compatible withes5 enableES5() + 1.7 KB
MapwithSetIn response to support enableMapSet() + 2.83 KB
Data change monitoring, see (3 tracking changes) enablePatches() + 2.33 KB
Get full functionality support enableAllPlugins() + 6.87 KB

2 draft and current/the original

2.1 Draft Destruction Problem

When using Produce agent data, Draft can be accessed and modified at will in the current execution stack, as follows

const base = {
  x: 0
}

produce(base, draft= >{

  console.log(draft.x)      / / 0. . })Copy the code

But when Produce completes execution, draft is destroyed

produce(base, draft= >{

  console.log(draft.x)      / / 0

  setTimeout(() = >{
    console.log(draft.x)    // Uncaught TypeError: Cannot perform the "get" operation on the revoked agent
  }, 100)})Copy the code

2.2 the current

ImmerJs provides the Current API, which enables you to copy a draft so that the current data can be retrieved in another execution stack

import produce, { current } from 'immer'

const base = {
  x: 0
}

produce(base, draft= >{

  draft.x++

  const copy = current(draft)

  draft.x++

  console.log(copy.x)		/ / 1
  console.log(draft.x)		/ / 2

  setTimeout(() = >{
    console.log(copy.x)    / / 1
  }, 100)})Copy the code

2.3 the original

ImmerJs provides the Original API to retrieve draft modified original data, similar to Vue3’s toRaw

import produce, { original, current } from 'immer'

const base = {
  x: 0
}

produce(base, draft= >{

  draft.x++

  console.log(original(draft) === base)		// true

})
Copy the code

3 Tracking changes

Redux can listen for changes in the browser extension, using middleware to listen for changes, and ImmerJs can listen for changes in draft. The prerequisite is to enable the function

import { enablePatches } from 'immer'

enablePatches()
Copy the code

3.1 Produce monitors changes

After pathes is enabled, you can add a third parameter to Produce

typeState = { name? :string.age: number.attr: number[] addAttr? :string
}

// 1 creates an initial state
const state: State = {
  name: "name".age: 1.attr: []}// Used to save steps from state => nextState
const changes = []
// Used to save steps from nextState => state
const inverseChanges = []

const nextState = produce(
  state,
  draft= > {
    // Modify the element
    draft.age = 33
    // Add attributes
    draft.addAttr = 'addAttr'
    // Delete attributes
    delete draft['name']
    // Add elements
    draft.attr[1] = 1
  },
  // This function is called only once
  (patches, inversePatches) = >{ changes.push(... patches) inverseChanges.push(... inversePatches) } )console.log(JSON.stringify(changes, null.2))
console.log(JSON.stringify(inverseChanges, null.2))
Copy the code

As you can see, in changes, save the transition from state to nextState

InverseChanges saves the steps to restore from nextState to state

3.2 produceWithPatches

While writing a function specifically to store changes is a bit more cumbersome, ImmerJs provides a new API that returns nextState, Changes and inverseChanges all at once

import { produceWithPatches, enablePatches } from 'immer'

enablePatches()

const state = {
  age: 1
}

const [ nextState, changes, inverseChanges ] = produceWithPatches(state, (draft) = >{
    draft.age++
})
Copy the code

3.3 applyPatches

With Patches, developers can use these Pathes to convert to any phase type, and ImmerJs provides a new API called applyPatches to help develop this rapid transformation

import { produceWithPatches, enablePatches, applyPatches } from 'immer'

enablePatches()

const state = {
  age: 1
}

const [ nextState, changes, inverseChanges ] = produceWithPatches(state, (draft) = >{
    draft.age++
})

// Convert nextState to state
console.log(applyPatches(nextState, inverseChanges))  // { age: 1 }
// Convert state to nextState
console.log(applyPatches(state, changes))             // { age: 2 }
Copy the code

4 Immutable nature of data

The value returned by ImmerJs is immutable, similar to obejct. freeze, but obejct. freeze modiates only the first layer of the object as immutable, while ImmerJs returns all immutable values. To change, you still need to use Produce to modify them

import produce from 'immer'

const state = {
  age: {
    value: 1
  },
  name: {
    value: 1}}const nextState = produce(state, (draft) = > {
  draft.age.value++
})

nextState.name.value++  // Uncaught TypeError: Cannot assign to read only property 'value' of object '#<Object>'
nextState.age.value++   // Uncaught TypeError: Cannot assign to read only property 'value' of object '#<Object>'
Copy the code

However, data freezing can be turned off using the setAutoFreeze API provided by ImmerJs

import produce, { setAutoFreeze } from 'immer'

// Turn off data freezing
setAutoFreeze(false)
// Enable data freezing
// setAutoFreeze(true)

const state = {
  age: {
    value: 1
  },
  name: {
    value: 1}}const nextState = produce(state, (draft) = > {
  draft.age.value++
})

nextState.name.value++
nextState.age.value++

console.log(nextState)  // { age: {value: 3}, name: {value: 2} }
Copy the code

5 produce

5.1 Callback Return Value

In the first callback function of Produce, you can return a value directly

  • If this value is zeroundefined, then the returned value isstate
  • If this value is not zeroundefined, then the returned value isreturnThe value that comes out (immutable)
import produce from "immer"

const baseState = {
  list: [{name: '1'.age: 18 },
    { name: '2'.age: 19 },
    { name: '3'.age: 20 },
    { name: '4'.age: 21 },
    { name: '5'.age: 22}}]const nextState = produce(baseState, draft= > {
  return draft.list.filter(t= > t.age % 2)})console.log(nextState)
/ / /
// {name: "2", age: 19},
// {name: "4", age: 21}
// ]
Copy the code

If, that is, you want to return undefined, you can return a nothing provided by ImmerJs

import produce, { nothing } from "immer"

const state = {
  age: 1
}

const nextState = produce(state, draft= > {
  return nothing
})

console.log(nextState)   // undefined
Copy the code

5.2 One-line writing

Because of this 5.1 feature of Produce, if you want to modify only one element, and this method wants to write only one line, you get a problem

const baseState = {
  age: 1
}

const nextState = produce(baseState, draft= > draft.age = 2)
Copy the code

The following methods can be used

const baseState = { age: 1 }

const nextState = produce(baseState, draft= > void (draft.age = 2))

console.log(nextState)   // { age: 2 }
Copy the code

Multi-line operations are as follows

const baseState = { age: 1 }

const nextState = produce(baseState, draft= > void ((draft.age++), (draft.age++)))

console.log(nextState)   // { age: 3 }
Copy the code

5.3 the asynchronous produce

Produce can also pass in asynchronous methods, such as now preparing a Node server

const { createServer } = require('http')

const server = createServer(function (request, response) {
  response.setHeader('Access-Control-Allow-Origin', request.headers.origin)
  response.setHeader('Access-Control-Allow-Headers'.'content-type')
  response.setHeader('Access-Control-Allow-Methods'.'DELETE,PUT,POST,GET')
  response.setHeader('Content-Type'.'application/json; charset=utf-8')
  response.end(JSON.stringify([
    { name: '1'.age: 1 },
    { name: '2'.age: 2 },
    { name: '3'.age: 3 },
    { name: '4'.age: 4}})))// Start the service
server.listen(3000.function (err) {
  if (err) {
    console.log('error', err)
  } else {
    console.log('Server started successfully http://127.0.0.1:3000')}})Copy the code

As follows, if Produce finds that a Promise is returned in the callback function, then produce will return a Promise

import produce from "immer"

const baseState = { list: []}async function my(){
  const nextState = await produce(baseState, async draft => {
    draft.list = await (await fetch('http://127.0.0.1:3000')).json()
  })
}

my()
Copy the code

6 createDraft/finishDraft

Data changes can be implemented without Produce or produceWithPatches, and ImmerJs provides two apis for this functionality

import { createDraft, finishDraft } from "immer"

const baseState = { age: 1 }
const draft = createDraft(baseState)
draft.age++
const nextState = finishDraft(draft)

console.log(baseState)    // { age: 1 }
console.log(nextState)    // { age: 2 }
Copy the code