immutability-helper vs seamless-immutable vs immer
State Management and Immutability in JavaScript
immutability-helperseamless-immutableimmerSimilar Packages:

State Management and Immutability in JavaScript

State management libraries in JavaScript help developers manage and maintain the state of an application in a predictable way. They provide tools and patterns for updating, reading, and synchronizing state across different parts of an application, making it easier to build complex user interfaces, handle asynchronous data, and ensure that the UI reflects the current state of the application. These libraries often implement concepts like immutability, time travel debugging, and centralized state storage to improve the reliability and maintainability of the code. immer is a popular library that allows developers to work with immutable state in a more intuitive way by using a mutable-like syntax, while immutability-helper provides a simple and efficient way to update nested immutable data structures using a concise and declarative API. seamless-immutable is a lightweight library that creates deeply immutable objects and arrays, ensuring that the original data cannot be modified, which helps prevent accidental mutations and makes the code more predictable.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
immutability-helper1,057,2195,219-66 years agoMIT
seamless-immutable482,9855,349-548 years agoBSD-3-Clause
immer028,913913 kB612 months agoMIT

Feature Comparison: immutability-helper vs seamless-immutable vs immer

Immutability Model

  • immutability-helper:

    immutability-helper provides a declarative way to update nested immutable data using a simple syntax. It allows you to specify the changes you want to make at any level of the object, and it handles creating the new immutable structure for you.

  • seamless-immutable:

    seamless-immutable creates deeply immutable objects and arrays by recursively freezing the data. Once an object is made seamless-immutable, it cannot be changed, ensuring that all levels of the structure are protected from mutations.

  • immer:

    immer uses a proxy-based approach to allow mutable-like updates while keeping the original state immutable. This means you can write code that looks like it modifies the state directly, but immer tracks the changes and produces a new immutable state object.

Ease of Use

  • immutability-helper:

    immutability-helper has a simple and straightforward API that makes it easy to update nested data structures. Its declarative nature helps keep the code clean and understandable.

  • seamless-immutable:

    seamless-immutable is very easy to use for creating immutable data. However, since it does not provide any tools for updating the data, developers need to be mindful of how they handle state changes.

  • immer:

    immer is easy to use, especially for developers who are familiar with mutable programming. Its API allows for intuitive state updates without needing to understand the complexities of immutability.

Performance

  • immutability-helper:

    immutability-helper is lightweight and performs well for updating nested data structures. It is designed to be efficient, but performance can vary depending on the complexity of the updates being made.

  • seamless-immutable:

    seamless-immutable is very fast for creating immutable objects, but since it creates a new copy of the data structure for every update, it is important to use it judiciously in performance-sensitive applications.

  • immer:

    immer is efficient for most use cases, but its proxy-based approach can introduce some overhead, especially for very large objects or frequent updates. However, the trade-off is often worth it for the clarity it brings to state management.

Mutability Control

  • immutability-helper:

    immutability-helper does not allow mutability; it enforces immutability by creating new copies of the data structure based on the specified changes.

  • seamless-immutable:

    seamless-immutable enforces strict immutability by making the entire object and its properties read-only, preventing any form of mutation.

  • immer:

    immer allows for controlled mutability within the produce function, giving developers the flexibility to write code that feels mutable while maintaining immutability at the data structure level.

Code Example

  • immutability-helper:

    Example of using immutability-helper for nested updates:

    import update from 'immutability-helper';
    
    const initialState = { count: 0, user: { name: 'Alice', age: 25 } };
    
    const nextState = update(initialState, {
      count: { $set: initialState.count + 1 }, // Update count
      user: { name: { $set: 'Bob' }, age: { $set: 26 } }, // Update nested properties
    });
    
    console.log(nextState); // { count: 1, user: { name: 'Bob', age: 26 } }
    console.log(initialState); // { count: 0, user: { name: 'Alice', age: 25 } }
    
  • seamless-immutable:

    Example of creating immutable objects with seamless-immutable:

    import seamless from 'seamless-immutable';
    
    const immutableObj = seamless({ name: 'Alice', age: 25 });
    const immutableArr = seamless([1, 2, 3]);
    
    console.log(immutableObj.name); // Alice
    console.log(immutableArr[0]); // 1
    
    // Attempting to mutate will fail silently (in strict mode) or throw an error
    immutableObj.name = 'Bob'; // No error, but the change is ignored
    immutableArr[0] = 10; // No error, but the change is ignored
    
    console.log(immutableObj.name); // Alice
    console.log(immutableArr[0]); // 1
    
  • immer:

    Example of using immer for state updates:

    import produce from 'immer';
    
    const initialState = { count: 0, user: { name: 'Alice' } };
    
    const nextState = produce(initialState, draft => {
      draft.count += 1; // Mutating the draft
      draft.user.name = 'Bob'; // Mutating nested property
    });
    
    console.log(nextState); // { count: 1, user: { name: 'Bob' } }
    console.log(initialState); // { count: 0, user: { name: 'Alice' } }
    

How to Choose: immutability-helper vs seamless-immutable vs immer

  • immutability-helper:

    Choose immutability-helper if you need a lightweight and straightforward solution for updating nested immutable data structures. It is ideal for projects where you want to keep the code simple and readable while still adhering to immutability principles.

  • seamless-immutable:

    Choose seamless-immutable if you want a minimalistic library that creates completely immutable objects and arrays. It is best for scenarios where you need to ensure that data cannot be changed at all, providing a strong guarantee against mutations.

  • immer:

    Choose immer if you want to work with immutable state in a more natural way, using a mutable-like syntax while still ensuring immutability under the hood. It is particularly useful for complex state updates where you want to avoid the boilerplate of manually creating new copies of nested objects.

README for immutability-helper

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

:warning: 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.thing2.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 */);