ImmutableJS is a library from Facebook that provides a series of immutable data structures. Updating a collection gives us a new version while leaving the original collection unchanged. Ordinary JavaScript objects stored inside it can still be mutable, though; we’ll distinguish those from nested immutable collections below. This means you can build predictable and reliable state models. This makes it easier to manage your application state.
More here: Immutable.js More about Immutable Data and React React.js Conf 2015 - Immutable Data and React - YouTube
Why ImmutableJS?
- Structural sharing can make updates and comparisons efficient
- Tracking mutation and maintaining state is difficult
- Encourages you to think differently about how data flows through your application
- Less error prone
- Simplified development
- Predictable
- Performance enhancements, Optimizations
- Mutation tracking
These are potential benefits, not a promise that every operation will be faster. The result depends on the workload, including how often we convert between ordinary JavaScript values and immutable collections.
Getting Started
The examples below are independent snippets. They use the Immutable namespace; in a module, start with import * as Immutable from 'immutable'. A bare Map means Immutable.Map, not JavaScript’s built-in Map.
ImmutableJS API is quite big. We will try to cover the basics and a bit more to show its power.
ImmutableJS provides many Persistent Immutable data structures including: List(), Stack(), Map(), OrderedMap(), Set(), OrderedSet() and Record().
We will cover Map() and List(), and describe the behavior of Seq() with Range().
Map()
- Read values
- Read deep values
- Change Values
- Change deep values
- Conversion to JavaScript types
- Member
Creates a new Immutable Map. An Object graph. Map - Immutable.js
const data = {
'one': {
title: 'One',
value: 1
},
'two': {
title: 'Two',
value: 2
}
}
let map = Immutable.Map(data)
Here Map(data) converts the outer collection only: map.get("one") is still an ordinary object. Use Immutable.fromJS(data) to convert nested plain objects and arrays too; you would then read the title with map.getIn(["one", "title"]).
get()
Returns the value associated with the provided key. Reading a value does not change the Map. get() - Immutable.js
map.get("one").title;
let obj = { 1: "one" };
Object.keys(obj); // [ "1" ]
obj["1"]; // "one"
obj[1]; // "one"
let map = Map(obj);
map.get("1"); // "one"
map.get(1); // undefined
getIn()
To get data from a deeply nested structure. getIn() - Immutable.js
With a Map()
let map = Immutable.fromJS({
title: 'Todo One',
text: 'Do todo',
category: {
title: 'Some category',
order: 1
}
})
map.getIn(['category', 'title']) // 'Some category'
length - size
To get the size of a Map() or a List()
map.size;
set()
const nextMap = map.set("three", { title: "three", value: 3 });
map.has("three"); // false: the original is unchanged
nextMap.has("three"); // true
delete()
const withoutOne = map.delete("one");
withoutOne.has("one"); // false
map.has("one"); // true
update()
const updatedMap = map.update('one', item => ({ ...item, value: item.value + 1 }));
updatedMap.get('one').value; // 2
map.get('one').value; // 1
clear()
Returns a new Map containing no keys or values.
const emptyMap = map.clear();
emptyMap.size; // 0
map.size; // 2
merge()
Returns a new Map resulting from merging the provided iterables.
let mapX = Immutable.Map({ a: 10, b: 20, c: 30 });
let mapY = Immutable.Map({ a: 50, b: 40, d: 60 });
mapX.merge(mapY).toObject(); // { a: 50, b: 40, c: 30, d: 60 }
Keep the return value of an update when you need the changed collection. Calling set, delete, or clear without using the result leaves your original map unchanged.
Querying Methods
For the remaining Map examples, let’s use this collection:
const items = Immutable.Map({
first: { id: "first", value: 1, completed: false },
second: { id: "second", value: 2, completed: true },
third: { id: "third", value: 3, completed: false }
});
has
Returns whether the key exists
items.has("first"); // true
first
Returns the first value in the Map’s iteration order. A Map does not promise insertion order; use OrderedMap or a List when order is meaningful.
map.first();
Iteration Methods
We can transform collections with .filter, .map, and .reduce. Use .forEach for side effects; it does not return a transformed collection.
groupBy
Groups values by the result of the callback
items.groupBy((item) => {
return item.completed;
});
Working with Subsets of a Map()
“First” and “last” below refer to iteration order, not the order in which entries were added.
slice()
Returns the last two items of a Map()
slice(<from>, <to>)
items.slice(items.size - 2, items.size);
takeLast()
Returns the last two items of a Map()
items.takeLast(2);
butLast()
Returns all items except the last
items.butLast();
rest()
items.rest();
skip()
Returns a Map() skipping the first 5 items
items.skip(5);
skipUntil()
Returns a Map() starting at the first value that matches the predicate
items.skipUntil((item) => item.value === 1);
skipWhile()
Skips consecutive values that match the predicate, stopping at the first that does not.
items.skipWhile((item) => item.value === 1);
Equality Methods
is()
let mapX = Immutable.Map({ a: 10, b: 20, c: 30 });
let mapY = Immutable.Map({ a: 10, b: 20, c: 30 });
Immutable.is(mapX, mapY); // true
FromJS
Object to Map()
Creates deeply nested Map() from a plain Javascript Object
let object = { a: 10, b: 20, c: 30 };
Immutable.fromJS(object); // Map()
Array to List()
Creates List() from a JS Array
let array = [10, 20, 30];
Immutable.fromJS(array); // List()
Usage of the reviver function
The reviver function takes a key and a value. Converting JS to Map() or List()
let array = [10, 20, 30];
Immutable.fromJS(array, (key, value) => {
return value.toMap();
}); // Map()
Note: the getIn will be index based instead of object based if it comes from an array
List()
Most of the Map() methods can be used with List() But there are some differences.
Differences between the Immutable Map() and List()
List() offers many familiar array-like operations. Its update methods return a new list rather than mutating the original.
Usually we wouldn’t use the push method in immutable data structures but with Immutable.List()s push methods are safe to be used.
let list = Immutable.List();
list = list.push(3);
list.toArray(); // [3]
get() and getIn()
The get method with Map() is key based and with List() is index based.
// get()
let list = Immutable.List();
list = list.push(3);
list.get(0); // 3
let map = Immutable.Map();
map = map.set("active", true);
map.get("active"); // true
// getIn()
let nestedList = Immutable.fromJS([10, 20, 30, [40, 50]]);
nestedList.getIn([3, 1]); // 50
of()
We can create a List() by using the of method
const items = [];
const list = Immutable.List.of("red", "green", "blue");
Using the spread operator:
const items = ["red", "green", "blue"];
const list = Immutable.List.of(...items);
Sequences
Represents a sequence of values. Seq() - Immutable.js
- Sequences are immutable — Once a sequence is created, it cannot be changed.
- Sequences are Lazy
Creating sequences with of()
let range = Array.from({ length: 1000 }, (_, index) => index);
let sequence = Immutable.Seq.of(...range)
For Example: the following performs no work, because the resulting of the sequence values are never iterated:
let operations = 0;
let squared = sequence.map((num) => {
operations++;
return num * num;
});
operations; // 0
// Now using the sequence
squared.take(10).toArray();
operations; // 10
Once the sequence is used, it performs only the work necessary. It will return it only when you ask for them.
This is really powerful because it doesn’t produce an overflow when working with an infinite range lazily.
let squaredRange = Immutable.Range(1, Infinity);
squaredRange.size; // Infinity
let first1000squared = squaredRange.take(1000).map((n) => n * n);
first1000squared.size; // 1000
Seq() allows for the efficient chaining of operations
let squaredOdds = Immutable.Range(0, Infinity)
.filter((n) => n % 2 !== 0)
.map((n) => n * n)
.take(1000);
console.log(squaredOdds.toArray());
You can find this example here: Sequences - JS Bin
Memoization with Immutable JS
Sequences are lazy and do not cache their results automatically. If we need to reuse a finite sequence, cacheResult() evaluates it once and keeps its values in memory.
let operations = 0;
const seq = Immutable.Range(1, Infinity)
.take(1000)
.map((n) => {
operations++;
return { value: n };
});
seq.cacheResult();
operations; // 1000
seq.toArray();
seq.toArray();
operations; // Still 1000: the mapped values were cached
Always limit an infinite sequence before caching it.