1. imh

imh

imh (Immutable Helper)

The extremely fast immutable helper

Installation

$ npm install --save imh

Benchmarks

Single mutation

Library Read Write Total
immutable.js (fastest) 265 372 637
imh 59 635 694
timm 43 662 705
immutable-assign 58 806 864
immhelper 55 1,049 1,104
seamless-immutable (production) 56 13,630 13,686
immer 46 18,386 18,432
update-immutable 44 38,532 38,576
immutability-helper 50 38,666 38,716

Multiple mutations

Library Read Write Total
update-immutable (fastest) 1 97 98
imh 1 198 199
immutability-helper 3 263 266
immhelper 0 303 303
immutable.js 170 538 708
immer 1 1,151 1,152
timm 2 1,710 1,712

Hence, what I recommend (from top to bottom):

  1. If you don't need immutability, well... just mutate in peace! I mean, in place
  2. If you need a complete, well-tested, rock-solid library and don't mind using a non-native API for reads: use ImmutableJS
  3. If you value using plain arrays/objects above other considerations, use imh
  4. If your typical use cases involve much more reading than writing, use imh as well
  5. If you do a lot of writes, updating items in long arrays or attributes in fat objects, use ImmutableJS

Usage

 import imh from "imh";

let state = {
  todos: [{ id: 1, title: "Todo 1", completed: false }],
  stats: {
    all: 1,
    active: 1,
    completed: 0,
  },
};

const StatsMutation = (current) =>
  // mutate stats prop
  imh.prop("stats", {
    all: current.todos.length,
    // compute number of active todos
    active: current.todos.filter((todo) => !todo.completed).length,
    // compute number of completed todos
    completed: current.todos.filter((todo) => todo.completed).length,
  });

function AddTodo(id, title) {
  state = imh(state, [
    // push new item to todos array
    imh.prop("todos", imh.push({ id, title, completed: true })),
    // update stats
    StatsMutation,
  ]);
}

function ToggleTodo(id) {
  state = imh(state, [
    // perform toggle action
    imh.prop(
      // nested prop path
      [
        // todos prop
        "todos",
        // toggle item which has id equal to given id
        (todo) => todo.id === id,
        // completed prop
        "completed",
      ],
      // toggle boolean value: true => false, false => true
      imh.toggle()
      // we can pass arrow function to mutate value as well
      // completed => !completed
    ),
    // update stats
    StatsMutation,
  ]);
}

AddTodo(2, "Todo 2");
console.log(state);
/*
{
  todos: [
    { id: 1, title: 'Todo 1', completed: false },
    { id: 2, title: 'Todo 2', completed: false },
  ],
  stats: { all: 2, active: 2, completed: 0 }
}
*/

AddTodo(3, "Todo 3");
console.log(state);
/*
{
  todos: [
    { id: 1, title: 'Todo 1', completed: false },
    { id: 2, title: 'Todo 2', completed: false },
    { id: 3, title: 'Todo 3', completed: false }
  ],
  stats: { all: 3, active: 3, completed: 0 }
}
*/

ToggleTodo(3);
console.log(state);
/*
{
  todos: [
    { id: 1, title: 'Todo 1', completed: false },
    { id: 2, title: 'Todo 2', completed: false },
    { id: 3, title: 'Todo 3', completed: true }
  ],
  stats: { all: 3, active: 2, completed: 1 }
}
*/

API References

imh(obj, mutation/mutations)

 imh(1, imh.add(1));
// => 2

// using val() to tell imh that is literal value (not mutation)
imh({ username: "admin", password: "admin" }, [
  imh.prop("password", imh.val("123456")),
  imh.prop("updatedOn", imh.val(Date.now())),
]);

// using custom mutation (a pure function that returns new value)
imh({ username: "admin", password: "admin" }, [
  imh.prop("password", () => "123456"),
  imh.prop("updatedOn", () => Date.now()),
]);

// using set() to update object property
imh({ username: "admin", password: "admin" }, [
  imh.set("password", "123456"),
  imh.set("updatedOn", Date.now()),
]);

imh(mutation/mutations)

Create imh wrapper function

 const AddTen = imh(imh.add(10));
AddTen(1);
// => 11

Array

push(...items)

 imh([1, 2, 3], imh.push(4, 5, 6));
// => [1, 2, 3, 4, 5, 6]

map(mutation/mutations)

 const todos = [
  { id: 1, title: "Todo 1" },
  { id: 2, title: "Todo 2" },
];
imh(
  todos,
  imh.map((todo) => ({ ...todo, title: todo.title.toUpperCase() }))
);
// => [ { id: 1, title: "TODO 1" }, { id: 2, title: "TODO 2" } ]

imh(todos, imh.map(imh.prop("title", imh.lower())));
// => [ { id: 1, title: "todo 1" }, { id: 2, title: "todo 2" } ]

splice(index, length, ...newItems)

 imh([1, 2, 3, 4, 5], imh.splice(2, 2));
// => [1, 2, 5]

imh([1, 2, 3, 4, 5], imh.splice(2, 2, 9, 10));
// => [1, 2, 9, 10, 5]

filter(predicate)

 imh(
  [1, 2, 3, 4, 5],
  imh.filter((number) => number % 2 === 0)
);
// => [2, 4]

sort([compareFn])

 imh([3, 2, 1], imh.sort());
// => [1, 2, 3]

imh(
  [{ name: "banana" }, { name: "apple" }, { name: "watermelon" }],
  imh.sort((a, b) => (a.name > b.name ? 1 : a.name < b.name ? -1 : 0))
);
// => [{ name: "apple" }, { name: "banana" }, { name: "watermelon" }]

orderBy([selector[, direction]])

 // order by name ascending
imh(
  [{ name: "banana" }, { name: "apple" }, { name: "watermelon" }],
  imh.orderBy((item) => item.name)
);
// => [{ name: "apple" }, { name: "banana" }, { name: "watermelon" }]

// order by name descending
imh(
  [{ name: "banana" }, { name: "apple" }, { name: "watermelon" }],
  imh.orderBy((item) => item.name, -1)
);
// => [{ name: "watermelon" }, { name: "banana" }, { name: "apple" }]

swap(from, to)

 imh([1, 2, 3], imh.swap(0, 2));
// => [3, 2, 1]

remove(...indices)

 imh([1, 2, 3], imh.remove(0, 2));
// => [2]

clear()

 imh([1, 2, 3], imh.clear());
// => []

pop()

 imh([1, 2, 3], imh.pop());
// => [1, 2]

shift()

 imh([1, 2, 3], imh.shift());
// => [2, 3]

unshift(...items)

 imh([1, 2, 3], imh.unshift(-1, 0));
// => [-1, 0, 1, 2, 3]

reverse()

 imh([1, 2, 3], imh.reverse());
// => [3, 2, 1]

Object

prop(name, mutation) & val(value)

Update current / nested object property

 const model = { l1: { l2: { l3: { l4: 1 } } } };
imh(
  model,
  imh.prop("l1", imh.prop("l2", imh.prop("l3", imh.prop("l4", imh.val(2)))))
);

imh(
  model,
  imh.prop(
    "l1",
    imh.prop(
      "l2",
      imh.prop(
        "l3",
        imh.prop("l4", () => 2)
      )
    )
  )
);

imh(model, imh.prop("l1", imh.prop("l2", imh.prop("l3", imh.set("l4", 2)))));

imh(model, imh.prop(["l1", "l2", "l3", "l4"], imh.val(2)));

imh(
  model,
  imh.prop(["l1", "l2", "l3", "l4"], () => 2)
);

imh(model, imh.prop(["l1", "l2", "l3"], imh.set("l4", 2)));

set(key, value)

 imh({ name: "Peter" }, imh.set("name", "Spider Man"));
// => { name: 'Spider Man' }

imh([1, 2, 3], imh.set(1, 4));
// => [1, 4, 3]

unset(...keys)

 imh({ prop1: 1, prop2: 2, prop3: 3 }, imh.unset("prop1", "prop2"));
// => { prop3: 3 }

imh([1, 2, 3], imh.unset(1, 2));
// => [1, undefined, undefined]

merge(...values)

 imh({ p1: 1, p2: 2 }, imh.merge({ p1: 1, p2: 2 }));
// => { p1: 1, p2: 2 } nothing to change

imh({ p1: 1, p2: 2 }, imh.merge({ p1: 1 }, { p2: 2 }));
// => { p1: 1, p2: 2 } nothing to change

imh({ p1: 1, p2: 2 }, imh.merge({ p1: 5 }, { p1: 1 }));
// => { p1: 1, p2: 2 } nothing to change

imh({ p1: 1, p2: 2 }, imh.merge({ p3: 3 }));
// => { p1: 1, p2: 2, p3: 3 }

String

replace(findWhat, replaceWith)

 imh("banana, apple, watermelon, banana", imh.replace("banana", "orange"));
// => 'orange, apple, watermelon, banana`

imh("banana, apple, watermelon, banana", imh.replace("banana", /orange/g));
// => 'orange, apple, watermelon, orange`

upper()

 imh("Oop!!!", imh.upper());
// => OOP!!!

lower()

 imh("Oop!!!", imh.lower());
// => oop!!!

Misc

add()

 imh(1, imh.add(9));
// => 10

imh(
  new Date(2000, 1, 1),
  img.add({
    years: 1,
    months: 1,
    days: 1,
    hours: 12,
    minutes: 12,
    seconds: 12,
    milliseconds: 900,
  })
);
// => 2001/02/02 12:12:12:900

imh(
  // unix timestamp
  new Date(2000, 1, 1).getTime(),
  img.add({
    years: 1,
    months: 1,
    days: 1,
    hours: 12,
    minutes: 12,
    seconds: 12,
    milliseconds: 900,
  })
);
// => unix timestamp

toggle()

 imh({ completed: false }, imh.prop("completed", imh.toggle()));
// => { completed: true }

result(callback)

Get result of last mutation. It is often used with splice() / pop() / shift()

 let state = {
  sourceList: ["item 1", "item 2"],
  destList: ["item 3", "item 4"],
};

function move(index, count) {
  state = imh(state, [
    // remove some from sourceList
    imh.prop("sourceList", imh.splice(index, count)),
    // append to destList
    imh.result((result) => imh.prop("destList", imh.push(...result))),
  ]);
}

Dependencies