Skip to content

mutate a copy of data without changing the original source

License

Notifications You must be signed in to change notification settings

mmorton/immutability-helper

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

28 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

immutability-helper

NPM version Build status Test coverage Downloads

Mutate a copy of data without changing the original source

This is a drop-in replacement for react-addons-update:

// import update from 'react-addons-update';
import update from 'immutability-helper';

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

Note that this module has nothing to do with React. However, since this module is most commonly used with React, the docs will focus on how it can be used with React.

Overview

React lets you use whatever style of data management you want, including mutation. However, if you can use immutable data in performance-critical parts of your application it's easy to implement a fast shouldComponentUpdate() method to significantly speed up your app.

Dealing with immutable data in JavaScript is more difficult than in languages designed for it, like Clojure. However, we've provided a simple immutability helper, update(), that makes dealing with this type of data much easier, without fundamentally changing how your data is represented. You can also take a look at Facebook's Immutable.js and React’s Using Immutable Data Structures section for more detail on Immutable.js.

The Main Idea

If you mutate data like this:

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

You have no way of determining which data has changed since the previous copy has been overwritten. Instead, you need to create a new copy of myData and change only the parts of it that need to be changed. Then you can compare the old copy of myData with the new one in shouldComponentUpdate() using triple-equals:

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

Unfortunately, deep copies are expensive, and sometimes impossible. You can alleviate this by only copying objects that need to be changed and by reusing the objects that haven't changed. Unfortunately, in today's JavaScript this can be cumbersome:

const newData = extend(myData, {
  x: extend(myData.x, {
    y: extend(myData.x.y, {z: 7}),
  }),
  a: extend(myData.a, {b: myData.a.b.concat(9)})
});

While this is fairly performant (since it only makes a shallow copy of log n objects and reuses the rest), it's a big pain to write. Look at all the repetition! This is not only annoying, but also provides a large surface area for bugs.

update()

update() provides simple syntactic sugar around this pattern to make writing this code easier. This code becomes:

import update from 'immutability-helper';

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

While the syntax takes a little getting used to (though it's inspired by MongoDB's query language) there's no redundancy, it's statically analyzable and it's not much more typing than the mutative version.

The $-prefixed keys are called commands. The data structure they are "mutating" is called the target.

Available Commands

  • {$push: array} push() all the items in array on the target.
  • {$unshift: array} unshift() all the items in array on the target.
  • {$splice: array of arrays} for each item in arrays call splice() on the target with the parameters provided by the item.
  • {$set: any} replace the target entirely.
  • {$merge: object} merge the keys of object with the target.
  • {$apply: function} passes in the current value to the function and updates it with the new returned value.

Examples

Simple push

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

initialArray is still [1, 2, 3].

Nested collections

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]}]

This accesses collection's index 2, key a, and does a splice of one item starting from index 1 (to remove 17) while inserting 13 and 14.

Updating a value based on its current one

const obj = {a: 5, b: 3};
const newObj = update(obj, {b: {$apply: function(x) {return x * 2;}}});
// => {a: 5, b: 6}
// This is equivalent, but gets verbose for deeply nested collections:
const newObj2 = update(obj, {b: {$set: obj.b * 2}});

(Shallow) Merge

const obj = {a: 5, b: 3};
const newObj = update(obj, {$merge: {b: 6, c: 7}}); // => {a: 5, b: 6, c: 7}

The main difference this module has with react-addons-update is that you can extend this to give it more functionality:

update.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 }));

Note that original in the function above is the original object, so if you plan making a mutation, you must first shallow clone the object. Another option is to use update to make the change return update(original, { foo: {$set: 'bar'} })

If you don't want to mess around with the globally exported update function you can make a copy and work with that copy:

import { newContext } from 'immutability-helper';
const myUpdate = newContext();
myUpdate.extend('$foo', function(value, original) {
  return 'foo!';
});

About

mutate a copy of data without changing the original source

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • JavaScript 100.0%