This article is basically a direct translation of immutability- Helpergithub official MD. If you are interested, you can also directly click the link to jump to the past.

Immutability-helper is an alternative to react- Addons-update, which is very helpful for the use of state in React. However, I have not found the real official document and Chinese document, so this article appears. At this stage, we will first translate a version, and then add some small examples to fully introduce the use of this module.

Installation and use:

npm install immutability-helper --save
/ / or...
yarn add immutability-helper
Copy the code

Create a copy of the data without changing the source data

import update from 'immutability-helper';

const state1 = ['x'];
const state2 = update(state1, {$push: ['y']}); // ['x', 'y']
Copy the code

Brief introduction:

React allows you to use any data management style you want, including mutable data. However, if you can use immutable data in a performance-critical part of your application, it’s easy to implement a quick shouldComponentUpdate() method to significantly speed up your application.

Working with immutable data in JavaScript is more difficult than working with a language like Clojure. However, we provide a simple immutable helper update() that makes it much easier to work with this type of data without fundamentally changing how the data is represented. You can also look at Facebook’s Immutable. Js and the React section Using Immutable Data Structures for more details about Immutable.

Core ideas:

When you use mutable data like this:

myData.x.y.z = 7;
/ / or...
myData.a.b.push(9);
Copy the code

When the last copy is overwritten, you will not be able to determine which data was changed. Instead, you need to create a new copy of myData and only modify the parts that need to be modified. In this case you can use congruence in shouldComponentUpdate() to compare the new data with the old copy of myData:

const newData = deepCopy(myData);
newData.x.y.z = 7;
newData.a.b.push(9);
Copy the code

Unfortunately, deep copy is not only expensive, but sometimes impossible to use. You can mitigate this by copying only the objects that need to be changed and reusing the ones that are not. Unfortunately, in today’s JavaScript, this can be troublesome:

const newData = Object.assign({}, myData, {
  x: Object.assign({}, myData.x, {
    y: Object.assign({}, myData.x.y, {z: 7})}),a: Object.assign({}, myData.a, {b: myData.a.b.concat(9)})});Copy the code

While this is fairly high performance (because it only generates shallow copies of log N objects and reuses the rest), it can be a lot of pain to write. Look at all the repetition! Not only is this annoying, it also provides a large area of code for bugs.

update()

Update () provides simpler syntactic sugar around this pattern, making it easier to write this code. The code becomes:

import update from 'immutability-helper';

const newData = update(myData, {
  x: {y: {z: {$set: 7}}},
  a: {b: {$push: [9]}}});Copy the code

While it takes some time to get used to the syntax (even if it is inspired by MongoDB’s queries), it has no redundant code, is statically parsed and doesn’t have much more type than the variable attribute version.

Keys prefixed with $are called commands. The “mutated” data structure is called the target.

Available commands

  • {$push: array}The effect is like callingpush()Method will bearrayAll items are appended to the target.
  • {$unshift: array}The effect is like callingunshift()Method will bearrayIs appended to the target.
  • {$splice: array of arrays}cyclearrays, using each of its items as argumentssplice()Methods.Note:**arraysThe items in are called sequentially, so their order is important. The subscript of the target may change during operation.
  • {$set: any}Completely replace the value of the target
  • {$toggle: array of strings}Invert the Boolean value of an object based on each item in the array.
  • {$unset: array of strings}According to thearrayRemoves an attribute from the target object.
  • {$merge: object}objectIs merged into the target.
  • {$apply: function}Pass the current value as an argument to the function and the return value of the function as the new value of the target
  • {$add: array of objects}MaporSetAdd value (not supportedWeakMaporWeakSet). As to theSetTo add, you need to pass in an array of arbitrary values to operate onMapTo add, you need to pass in a structure for[key, value]An array of format looks like this:update(myMap, {$add: [['foo', 'bar'], ['baz', 'boo']]}).
  • {$remove: array of strings}Based on the key pair of the list speciesMaporSetIs deleted.
$the apply of shorthand

In addition, you can pass in a function instead of a command object, which is treated as a command object using $apply: update({a: 1}, {a: function}). This example equals update({a: 1}, {a: {$apply: function}}).

limitations

⚠️ update applies only to data properties and not to accessor properties defined via Object.defineProperty. He may not see the latter, resulting in the creation of hidden data attributes, and he may cause the application logic to be corrupted as a side effect of setters. Therefore, update can only be applied to pure data objects with data Properties as a subset.

An 🌰

Simple push
const initialArray = [1.2.3];
const newArray = update(initialArray, {$push: [4]}); // => [1, 2, 3, 4]
Copy the code

InitialArray values are still [1, 2, 3].

Nested set
const collection = [1.2, {a: [12.17.15]}];
const newCollection = update(collection, {2: {a: {$splice: [[1.1.13.14]]}}});
// => [1, 2, {a: [12, 13, 14, 15]}]
Copy the code

The entry here is for items with collection index value 2, key name A, start subscript 1(remove items with value 17) and insert 13 and 14 values.

Update based on the current value
const obj = {a: 5.b: 3};
const newObj = update(obj, {b: {$apply: function(x) {return x * 2; }}});// => {a: 5, b: 6}
// The effect is the same here, but it is very redundant for deep nesting:
const newObj2 = update(obj, {b: {$set: obj.b * 2}});
Copy the code
(Shallow) merge
const obj = {a: 5.b: 3};
const newObj = update(obj, {$merge: {b: 6.c: 7}}); // => {a: 5, b: 6, c: 7}
Copy the code
Compute attribute name

You can use index values through runtime variables through ES2015’s Computed Property Names feature. Object property name expressions can be wrapped around square brackets [], which will get the value at run time as the final property name.

const collection = {children: ['zero'.'one'.'two']};
const index = 1;
const newCollection = update(collection, {children: {[index]: {$set: 1}}});
// => {children: ['zero', 1, 'two']}
Copy the code
Removes an element from an array
// Delete an item with a specific index value, regardless of its value
update(state, { items: { $splice: [[index, 1]]}});Copy the code

Autovivification

Autovivification is a creator that automatically creates new arrays and objects when needed. In a JavaScript context, this means something like this:

const state = {}
state.a.b.c = 1; // State is equivalent to {a: {b: {c: 1}}}
Copy the code

Since JavaScript does not have this “feature”, immutability-Helper does the same. This is almost impossible in JavaScript and immutability-Helper extensions for the following reasons:

var state = {}
state.thing[0] = 'foo' // what type should state.thing be? Should it be an array or object?
state.thing2[1] = 'foo2' // What about thing2 He's definitely supposed to be an object!
state.thing3 = ['thing3'] // This is normal JS syntax, which is not based on autovivification
state.thing3[1] = 'foo3' // emmmmm, remember that state.thing2 is an object, but this is an array
state.thing2.slice // it should be undefined
state.thing2.slice // it should be function
Copy the code

If you need to assign to a deeply nested content and don’t want to do it one layer at a time, consider this technique. Here’s a quick example:

var state = {}
var desiredState = {
  foo: [{bar: ['x'.'y'.'z']},]};const state2 = update(state, {
  foo: foo= >
    update(foo || [], {
      0: fooZero= >
        update(fooZero || {}, {
          bar: bar= > update(bar || [], { $push: ["x"."y"."z"]})})})});console.log(JSON.stringify(state2) === JSON.stringify(desiredState)) // true
// Note that state can be declared either of the following and still print true:
// var state = { foo: [] }
// var state = { foo: [ {} ] }
// var state = { foo: [ {bar: []} ] }
Copy the code

You can also choose to add $auto and $autoArray commands using extensions:

import update, { extend } from 'immutability-helper';

extend('$auto'.function(value, object) {
  return object ?
    update(object, value):
    update({}, value);
});
extend('$autoArray'.function(value, object) {
  return object ?
    update(object, value):
    update([], value);
});

var state = {}
var desiredState = {
  foo: [{bar: ['x'.'y'.'z']},]};var state2 = update(state, {
  foo: {$autoArray: {
    0: {$auto: {
      bar: {$autoArray: {$push: ['x'.'y'.'z']}}}}}}});console.log(JSON.stringify(state2) === JSON.stringify(desiredState)) // true
Copy the code

Add a custom command

The main difference between this module and react-addons-update is that this module allows you to extend more commands:

import update, { extend } from 'immutability-helper';

extend('$addtax'.function(tax, original) {
  return original + (tax * original);
});
const state = { price: 123 };
const withTax = update(state, {
  price: {$addtax: 0.8}}); assert(JSON.stringify(withTax) === JSON.stringify({ price: 221.4 }));
Copy the code

Note that original in the above function is the original object, so if you want it to be used as mutable data, you must first make a shallow copy of the object. The alternative is to update the return to update(original, {foo:{$set:{bar}})

If you don’t want to be confused with the globally exported update function, you can create your own copy and work with it:

import { Context } from 'immutability-helper';

const myContext = new Context();

myContext.extend('$foo'.function(value, original) {
  return 'foo! ';
});

myContext.update(/* args */);
Copy the code