本文介绍不可变数据类型ImmutableJS和ImmerJS的基本用法,以及它们在React项目中的使用。
This article was originally in Chinese, to imporve my English, I rewrite it today in English as a review.
November 4th, 2025
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.jswas 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.jsprovides an elegant solution to such problems.
Immutable Data: Data that cannot be changed once created.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 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 Collection、List、Map、Set、Record、Seq. It is very comprehensive of functional operations map、filter、groupBy、reduce、find. 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
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.
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
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.
javascriptconst { 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 , while comparison of reference types has an algorithm complexity of . Therefore, performance trade-offs should be considered when using them.
Immutable.js has an object oriented API that very similar to ES2015 Array, Map and Set.
| JavaScript | immutable |
|---|---|
| Map | Map |
| Set | Set |
| Array | List |
| Object | fromJS or Map |
javascriptconst { 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 ]
javascriptconst { 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 } } }
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.
javascriptconst { 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
javascriptconst { 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
Since immutable data is typically very deeply nested, Cursor provides a reference to this deep data to facilitate access to the deep data.
javascriptimport 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
javascriptfunction 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`
}
javascriptimport { 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
Undo/Redo,Copy/PasteImmutable.js tried to make the API design similar to native objects, it is sometimes difficult to distinguish between immutable object and native object.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.Here are some ways to avoid similar problems:
TypeScript that have static type checking;Immutable begin with $$;Immutable.fromJS instead of Immutable.Map or Immutable.List to create objects, which avoids mixing Immutable and native objects.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;deepCopy and deepCompare within shouldComponentUpdate lifecycle to avoid unnecessary render, but deepFn is also time-consuming.javascriptimport { 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;
}
ImmutableJS has two shortcomings in hand the side effects of modifying reference type:
Immer.js;javascriptlet 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.
produce(currentState, recipe: (draftState) => void | draftState, ?PatchListener): nextState
currentState: The initial state of the operated objectdraftState: 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 draftStateproduce: 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.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++;
}))
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 许可协议。转载请注明出处!