2023-09-05
ReactJS
00
请注意,本文编写于 1049 天前,最后修改于 250 天前,其中某些信息可能已经过时。

目录

Background: Why does something exist that is immutable?
What is immutable?
immutable.js
API Quick Start
Map Type
immutable type comparision
JavaScript-first API
Nested structure
Batch process mutable
Lazy sequence
cursor
Advantages
shortcoming
Optimize React with immutable.js
immer.js
Concept Explanation
Optimize React with immer.js
use immer in reduce

本文介绍不可变数据类型ImmutableJSImmerJS的基本用法,以及它们在React项目中的使用。

This article was originally in Chinese, to imporve my English, I rewrite it today in English as a review.
November 4th, 2025

Background: Why does something exist that is immutable?

JavaScript has reference types and primitive types.

  • If variable 'a' is a primitive type, assigning 'a' to variable 'b' and then modifying the value of 'b' will not effect variable 'a'.
  • But, if variable 'a' is a reference type, and 'a' is assigned to variable 'b' then 'b' and 'a' are references to the same object. If a's property of object is modified, that property of object 'b' is also modified, because they are some object.

    The solution is deep copy, such as the simplest method: JSON.parse(JSON.stringify(obj))
  • However, deep copying is quite performance-intensive(in terms of both space and time), is there a compromise solution? that is, modifying 'b' without effecting 'a', and also save performance compared to deep copying.
  • Thus, immutable.js was developed, which simply means on demand deep copying.
  • Another important reason is that functions often have side effects during development. The side effects involved functions that modify the value of a property of a reference type or perform a shallow copy. immutable.js provides an elegant solution to such problems.

What is immutable?

  • Immutable Data: Data that cannot be changed once created.
  • Any modification , addition, or deletion operation on an Immutable object will return a new Immutable object;
  • Immutable, its implementation principle is Persistent Data Structure持久化数据结构):In other words when creating new data using old data, it is necessary to that old data is both available and unchanged. At the same time, to avoid the performance loss caused by deepCopy copying all nodes, Immutable uses Structural Sharing结构共享), That is if a node in the object tree changes only that node and its parent nodes are modified, while other nodes are shared.

immutable.js

immutable is an immutable collection in Javascript
offical website

Facebook engineer Lee Byron spent 3 years building. It appeared at same time of React, but it wasn't inclued in React toolkit by default(React provide simplified Helper). It internally implements a complete set of Persistent Data Structure, There are also many easy-to-use data types. like CollectionListMapSetRecordSeq. It is very comprehensive of functional operations mapfiltergroupByreducefind. At the same time, the API should be as similar as possible to Object or Array.

javascript
// The original way of writing let foo = {a: {b: 1}}; let bar = foo; bar.a.b = 2; console.log(foo.a.b); // print 2 console.log(foo === bar); // print true // after import immutable.js import Immutable from 'immutable'; var foo = Immutable.fromJS({a: {b: 1}}); var bar = foo.setIn(['a', 'b'], 2); // assining value using `setIn` console.log(foo.getIn(['a', 'b'])); // using `getIn` to get value, print 1 // equivalent to `foo.getIn(['a']).getIn(['b'])` console.log(foo === bar); // print false

API Quick Start

Unable to find easily understandable and quick starting Chinese documentation on immutable.js, I had no choice but to read the documentation myself. The following is my own summary based on the official documentation.

javascript
// As mentioned earlier, the concept of immutable was introduced to eliminate the side effects of reference types and a shallow copies. // However, deep copying is a performance-intensive method // Therefore we are looking for a way that avoids side effects and saves costs -- structure sharing. // without immutable.js, we can avoid side effects like this var a = {b:'B',c:{d:1}}; var b = Object.assign({}, a); b.b = 'new val bbb'; console.log(b.b) // 'new val bbb' console.log(a.b); // 仍旧为 'B' console.log(a.c === b.c); // true // 显然 7,8行代码书写起来不方面, // 如果a.b也是引用类型,还对a.b 重复进行上述两部操作

However, using immutable.js has a learning curve as it defines some custom data structures.

Map Type

javascript
// If it's an object type, use map in immutable. const { Map } = require('immutable'); const map1 = Map({ a: 1, b: 2, c: [1,2] }); // Modifying a property of the original will create a new object, while the original object remains unchanged. const map2 = map1.set('b', 50); // use fuction `.equals` to compare console.log(map1.equals(map2)); // false // get value .get console.log(map1.get('b'), map2.get('b')); // 2, 50 console.log(map1.get('c') === map2.get('c')); // true, c is not change

immutable type comparision

Comparisons of reference types are based on their reference addresses, while comparision of immutable objects are based on whether the value in collection are identical.

javascript
const { Map } = require('immutable'); const map1 = Map({ a: 1, b: 2, c: 3 }); const map2 = Map({ a: 1, b: 2, c: 3 }); map1.equals(map2); // true // Immutable.is(map1, map2); // true map1 === map2; // false const map3 = map2.set('b', 2); map3 === map2; // true // if an object is immutable, // it can be copied simply by making another reference to it instead of copying the entire object. // This is because it can save memory and potentially improve the execution speed of programs that rely on copies (e.g: undoing stacks).

Additional information: Comparison of immutable objects is based on value and has an algorithm complexity of O(N)O(N), while comparison of reference types has an algorithm complexity of O(1)O(1). Therefore, performance trade-offs should be considered when using them.

JavaScript-first API

Immutable.js has an object oriented API that very similar to ES2015 Array, Map and Set.

JavaScriptimmutable
MapMap
SetSet
ArrayList
ObjectfromJS or Map
javascript
const { Map, List } = require('immutable'); const map1 = Map({ a: 1, b: 2, c: 3, d: 4 }); const map2 = Map({ c: 10, a: 20, t: 30 }); const obj = { d: 100, o: 200, g: 300 }; const map3 = map1.merge(map2, obj); console.log(map3.toJS()); // Map { a: 20, b: 2, c: 10, d: 100, t: 30, o: 200, g: 300 } const list1 = List([1, 2, 3]); const list2 = List([4, 5, 6]); const array = [7, 8, 9]; const list3 = list1.concat(list2, array); // As can be seen from the example above // 1. Align with JS API, such as Array.prototype.concat // 2. APIs can interact with Javascript objects. // All Immutable.js collections are iterable const { List } = require('immutable'); const aList = List([1, 2, 3]); const anArray = [0, ...aList, 4, 5]; // [ 0, 1, 2, 3, 4, 5 ]

Nested structure

javascript
const { fromJS } = require('immutable'); const nested = fromJS({ a: { b: { c: [3, 4, 5] } } }); console.log(typeof nested.toJS()); // 'object' const nested2 = nested.mergeDeep({ a: { b: { d: 6 } } }); // nested2 { a: { b: { c: [3, 4, 5] }, d: 6 } } console.log(nested2.getIn(['a', 'b', 'd'])); // 6 console.log(nested2.getIn(['a']).getIn(['b', 'd'])); // 6 const nested3 = nested2.updateIn(['a', 'b', 'd'], value => value + 1); console.log(nested3); // Map { a: Map { b: Map { c: List [ 3, 4, 5 ], d: 7 } } } const nested4 = nested3.updateIn(['a', 'b', 'c'], list => list.push(6)); // Map { a: Map { b: Map { c: List [ 3, 4, 5, 6 ], d: 7 } } }

Batch process mutable

By default, the api in immutable.js returns a new mutable object.
If I only need a final immutable object and don't want the intermediate objects, is it possible to simply return a new object?
This is where withMutations comes in handy, allow for batch processing and saving overhead. Currently, only a few methods, set, push,pop can be directly applied to persistent data structure.

javascript
const { List } = require('immutable'); const list1 = List([1, 2, 3]); const list2 = list1.withMutations(function (list) { list.push(4).push(5).push(6); }); console.log(list1.size); // 3 console.log(list2.size); // 6

Lazy sequence

javascript
const { Seq } = require('immutable'); const oddSquares = Seq([1, 2, 3, 4, 5, 6, 7, 8]) .filter(x => { console.log('filter', x) return x % 2 !== 0 }) .map(x => x * x); // because `oddSquares` is an immutable object, it is lazily executed, these code above will not perform any operation. // The code will execute when an immutable object is case to a javascript object. console.log(oddSquares.toJS()); // The console print 'filter' // Range is a special kind of `Lazy` sequent。 const { Range } = require('immutable'); const a = Range(990, 1010) // .skip(100) .map(n => { console.log(n) return -n; }) .reduce((r, n) => r * n, 1); console.log('---', a) // --- 9.897178826145609e+59

cursor

Since immutable data is typically very deeply nested, Cursor provides a reference to this deep data to facilitate access to the deep data.

javascript
import Immutable from 'immutable'; import Cursor from 'immutable/contrib/cursor'; let data = Immutable.fromJS({ a: { b: { c: 1 } } }); // 让 cursor 指向 { c: 1 } let cursor = Cursor.from(data, ['a', 'b'], newData => { // 当 cursor 或其子 cursor 执行 update 时调用 console.log(newData); }); cursor.get('c'); // 1 cursor = cursor.update('c', x => x + 1); cursor.get('c'); // 2

Supplementary content: November 5, 2025

“Cursor is an interesting pattern, but it belongs in a state management layer, not in the data structure library.”
—— Immutable.js team

Advantages

  1. Reduces the complexity introduced by mutable
javascript
function touchAndLog(touchFn) { let data = { key: 'value' }; touchFn(data); console.log(data.key); // Since we didn't know what operation touchFn is performing, it's impossible to predict. but use immutable, it's definitely `value` }
  1. Save Memory
    It will try to reuse memory, and even previously used objects can be reused.
    Objects that not referenced will be garbage collected.
javascript
import { Map} from 'immutable'; let a = Map({ select: 'users', filter: Map({ name: 'Cam' }) }) let b = a.set('select', 'people'); a === b; // false a.get('filter') === b.get('filter'); // true
  1. Undo/RedoCopy/Paste
    Since the data is different each time, it can be stored in an array and you can retrieve corresponding data to go back to any data 因为每次数据都是不一样的,所有可以存储在数组里,想回退到哪里就拿出对应数据即可

shortcoming

  1. Need to learn new APIs
  2. Easily cofused with native objects
    1. Although Immutable.js tried to make the API design similar to native objects, it is sometimes difficult to distinguish between immutable object and native object.
    2. While Map and List in immutable.js correspond to the native Object and Array, their options are very different. For example you should use map.get('key') instead of map.key, and array.get(0) instead of array[0]. Further, Immutable returns a new object every time it is modified, make it easy to forget to assign values.
    3. When using external libraries, it is generally necessary to use native objects, and it is easy to forget and to convert them.

Here are some ways to avoid similar problems:

  1. Using tools like TypeScript that have static type checking;
  2. Conventional variable naming rules: All objects of type Immutable begin with $$;
  3. use Immutable.fromJS instead of Immutable.Map or Immutable.List to create objects, which avoids mixing Immutable and native objects.

Optimize React with immutable.js

  1. React can use shouldComponentUpdate() for performance optimization, but it returns true by defalut, meaning that render() methos will always be executed, following by a Virtual DOM comparision to determine whether a Real DOM update is needed;
  2. You can execute deepCopy and deepCompare within shouldComponentUpdate lifecycle to avoid unnecessary render, but deepFn is also time-consuming.
javascript
import { is } from 'immutable'; shouldComponentUpdate: (nextProps = {}, nextState = {}) => { const thisProps = this.props || {}, thisState = this.state || {}; if (Object.keys(thisProps).length !== Object.keys(nextProps).length || Object.keys(thisState).length !== Object.keys(nextState).length) { return true; } for (const key in nextProps) { if (!is(thisProps[key], nextProps[key])) { return true; } } for (const key in nextState) { if (thisState[key] !== nextState[key] && !is(thisState[key], nextState[key])) { return true; } } return false; }

immer.js

offical documentation

ImmutableJS has two shortcomings in hand the side effects of modifying reference type:

  1. Users should learn how to manipulate its data structures, which is not as simple and easy to use as the native object manipulate provide by Immer.js;
  2. Its operation results can be obtained by using the toJS method to get the native value. This means that when operating on an object, you must always know whether you are operating on the native object or the return result of ImmutableJS. If you are not careful, problems will occur.
javascript
let currentState = { x:[2] } let o1 = currentState; o1.a = 1; let o2 = { ...currentState }; o2.x.push(3); console.log(currentState) // { x: [ 2, 3 ], a: 1 } /* use immer.js to solve the above problem */ import produce from 'immer'; let state = { x:[2] } let obj1 = produce(state, draft => { draft.a = 1; }); let obj2 = produce(state, draft => { draft.x.push(3); }); // { x: [ 2 ] } { x: [ 2 ], a: 1 } { x: [ 2, 3 ] } console.log(state, obj1, obj2); // What did produce function do? // Iterate through the original object and freeze each object in turn.

Concept Explanation

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

  • currentState:  The initial state of the operated object
  • draftState:   The draft state generated by currentState is a proxy of currentState. Any modifications made to draftState will be recorded and used to generate nextState . During this process, currentState will remain unaffected.
  • nextState:   The final state generated based on draftState
  • produce:    Function used to generate nextState or producer.
  • producer:    Generated via produce, used to produce nextState , which performs the same operation each time.
  • recipe:     Functions used to mainpulate draftState.

Optimize React with immer.js

javascript
// original writing setState const { members } = this.state; this.setState({ members: [ { ...members[0], age: members[0].age + 1, }, ...members.slice(1), ] }) // current writing style const { members } = this.state; this.setState(produce(members, draft => { draft.members[0].age++; }))

use immer in reduce

javascript
// produce 内的 recipe 回调函数的第2个参数与obj对象是指向同一块内存 function reducer (state = {name: '章三', age: 12}, action) { return immer.produce(state, (draft) => { switch(action.type) { case 'ADD': draft.age++; return default: return; } }) } const store = Redux.createStore(reducer); class App extends React.Component { componentDidMount() { store.subscribe(() => { this.forceUpdate(); }) } render() { const state = store.getState(); return <div> <p>{state.name}今年{state.age}周岁</p> <button onClick={() => store.dispatch({type: 'ADD'})}>过生日</button> </div> } } const root = ReactDOM.createRoot(document.getElementById('root')); root.render(<App/>);

完整案例 redux-immer.html

本文作者:郭敬文

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!