• Stars
    star
    5,174
  • Rank 8,013 (Top 0.2 %)
  • Language
    TypeScript
  • License
    MIT License
  • Created almost 9 years ago
  • Updated about 4 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

mutate a copy of data without changing the original source

immutability-helper

NPM version Build status Test coverage Downloads Minified size Gzip size

Mutate a copy of data without changing the original source

Setup via NPM

npm install immutability-helper --save

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 = 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)})
});

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. Note: The items in the array are applied sequentially, so the order matters. The indices of the target may change during the operation.
  • {$set: any} replace the target entirely.
  • {$toggle: array of strings} toggles a list of boolean fields from the target object.
  • {$unset: array of strings} remove the list of keys in array from the target object.
  • {$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.
  • {$add: array of objects} add a value to a Map or Set. When adding to a Set you pass in an array of objects to add, when adding to a Map, you pass in [key, value] arrays like so: update(myMap, {$add: [['foo', 'bar'], ['baz', 'boo']]})
  • {$remove: array of strings} remove the list of keys in array from a Map or Set.

Shorthand $apply syntax

Additionally, instead of a command object, you can pass a function, and it will be treated as if it was a command object with the $apply command: update({a: 1}, {a: function}). That example would be equivalent to update({a: 1}, {a: {$apply: function}}).

Limitations

⚠️ update only works for data properties, not for accessor properties defined with Object.defineProperty. It just does not see the latter, and therefore might create shadowing data properties which could break application logic depending on setter side effects. Therefore update should only be used on plain data objects that only contain data properties as descendants.

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}

Computed Property Names

Arrays can be indexed into with runtime variables via the ES2015 Computed Property Names feature. An object property name expression may be wrapped in brackets [] which will be evaluated at runtime to form 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']}

Removing an element from an array

// Delete at a specific index, no matter what value is in it
update(state, { items: { $splice: [[index, 1]] } });

Autovivification

Autovivification is the auto creation of new arrays and objects when needed. In the context of javascript that would mean something like this

const state = {}
state.a.b.c = 1; // state would equal { a: { b: { c: 1 } } }

Since javascript doesn't have this "feature", the same applies to immutability-helper. The reason why this is practically impossible in javascript and by extension immutability-helper is the following:

var state = {}
state.thing[0] = 'foo' // What type should state.thing have? Should it be an object or array?
state.thing2[1] = 'foo2' // What about thing2? This must be an object!
state.thing3 = ['thing3'] // This is regular js, this works without autovivification
state.thing3[1] = 'foo3' // Hmm, notice that state.thing2 is an object, yet this is an array
state.thing2.slice // should be undefined
state.thing3.slice // should be a function

If you need to set something deeply nested and don't want to have to set each layer down the line, consider using this technique which is shown with a contrived 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 could have been declared as any of the following and it would still output true:
// var state = { foo: [] }
// var state = { foo: [ {} ] }
// var state = { foo: [ {bar: []} ] }

You can also choose to use the extend functionality to add an $auto and $autoArray command:

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

Adding your own commands

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

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

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 { Context } from 'immutability-helper';

const myContext = new Context();

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

myContext.update(/* args */);

More Repositories

1

exercises

Some basic javascript coding challenges and interview questions
JavaScript
4,174
star
2

safetest

TypeScript
1,307
star
3

wavy

use ~ in require and import calls
JavaScript
379
star
4

nip

Node Input/output Piper
JavaScript
291
star
5

webwork

Execute Web Workers without external files
JavaScript
229
star
6

redux-await

Manage async redux actions sanely
JavaScript
155
star
7

zerobox

JavaScript
148
star
8

jsan

handle circular references when stringifying and parsing
JavaScript
90
star
9

redux-create-reducer

Publishing createReducer from http://redux.js.org/docs/recipes/ReducingBoilerplate.html#generating-reducers
JavaScript
84
star
10

weak-key

Get a unique key for an object ( mainly for react's key={} )
JavaScript
76
star
11

ts-cookbook

A collection of delicious Typescript recipes
TypeScript
44
star
12

screenliner

node util for writing to regions on the terminal
JavaScript
41
star
13

deact

react inspired DOM element template engine
JavaScript
33
star
14

TinyRouter.php

Simple Router For PHP
PHP
26
star
15

w8

Give promises and thunks timeouts
JavaScript
25
star
16

run-every

run a command over and over
JavaScript
23
star
17

crlf

get and set line endings - Pull Requests welcome
JavaScript
23
star
18

react-mocha-starter

Scaffolding for a react project with mocha testing in node and phantom
JavaScript
18
star
19

nester

Get and set properties from deeply nested arrays
JavaScript
16
star
20

zerovalidate

A minimalist javascript form validator
JavaScript
12
star
21

redux-browserify

it can be done
JavaScript
12
star
22

redux-standard-action

A human-friendly standard for Redux action objects.
JavaScript
11
star
23

jq2

extract json data
JavaScript
10
star
24

wttt

When This Then That
JavaScript
10
star
25

zan

test object types (similar to React.PropTypes)
JavaScript
7
star
26

httpies

httpie with extra headers
JavaScript
7
star
27

poel

Create a pool of cluster workers.
JavaScript
6
star
28

react-compose-wrappers

TypeScript
6
star
29

typeaheadObj

Twitter Bootstrap Typeahead With Objects
JavaScript
6
star
30

git-tree-maker

JavaScript
6
star
31

is_googlebot.php

PHP
4
star
32

zerocomplete

JavaScript
4
star
33

tweet-editor

HTML
3
star
34

mithril-todo-msx

JavaScript
3
star
35

fizzbuzz

JavaScript
3
star
36

reglob

require a glob
JavaScript
3
star
37

will-they-sue

list the license of all your deps
JavaScript
3
star
38

ccolors

Cli Colors, a slightly improved version of npm's colors
JavaScript
3
star
39

member-berry

Memoize a function of n args with O(n) recall and no memory leaks.
JavaScript
3
star
40

Db.class.php

PHP
2
star
41

react-track-events

TypeScript
2
star
42

babel-plugin-hodor

JavaScript
2
star
43

zerolay

JavaScript
2
star
44

jquery.save

jQuery.save
2
star
45

xurl

A simple url parser
JavaScript
2
star
46

es6-module-boilerplate

boilerplate for es6 -> es5 module
JavaScript
2
star
47

logen

lodash like library for generators
JavaScript
1
star
48

talks

HTML
1
star
49

Timer.class.php

PHP
1
star
50

CacheSQL

CacheSQL class
PHP
1
star
51

co-phantom

JavaScript
1
star
52

tuplizer

Powerful typescript types to manipulate tuples
TypeScript
1
star
53

wrt

A simple react router
JavaScript
1
star
54

torah

Python
1
star
55

webcam-switcher

TypeScript
1
star
56

promise-utils

Useful utils to use native promises like bluebird
JavaScript
1
star
57

isIE10

javascript test if browser is IE10
JavaScript
1
star
58

reflux-create-reducer

Publishing createReducer from http://rackt.github.io/redux/docs/recipes/ReducingBoilerplate.html
JavaScript
1
star
59

m2

Lightweight Mocha Clone
JavaScript
1
star
60

node-webkit-stater

JavaScript
1
star
61

rxjs-props

Like Promise.props but for rxjs
JavaScript
1
star
62

octopress-test-case

Shell
1
star
63

toopl

Typescript tuple
JavaScript
1
star
64

github-tab-resizer

JavaScript
1
star
65

react-override

TypeScript
1
star
66

require-es6

Require es6 version of module if available as `main-es6` prop in package.json
JavaScript
1
star