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 and
immutable.js
Types 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