8.5 Libraries for avoiding shared mutable state

There are several libraries available for JavaScript that support immutable data with non-destructive updating. Two popular ones are:

  • Immutable.js provides immutable data structures for lists, stacks, sets, maps, etc.
  • Immer also supports immutability and non-destructive updating but for plain objects, Arrays, Sets, and Maps. That is, no new data structures are needed.

These libraries are described in more detail in the next two sections.

8.5.1 Immutable.js

In its repository, the library Immutable.js is described as:

Immutable persistent data collections for JavaScript which increase efficiency and simplicity.

Immutable.js provides immutable data structures such as:

  • List
  • Stack
  • Set (which is different from JavaScript’s built-in Set)
  • Map (which is different from JavaScript’s built-in Map)
  • Etc.

In the following example, we use an immutable Map:

  1. import {Map} from 'immutable/dist/immutable.es.js';
  2. const map0 = Map([
  3. [false, 'no'],
  4. [true, 'yes'],
  5. ]);
  6. // We create a modified version of map0:
  7. const map1 = map0.set(true, 'maybe');
  8. // The modified version is different from the original:
  9. assert.ok(map1 !== map0);
  10. assert.equal(map1.equals(map0), false); // (A)
  11. // We undo the change we just made:
  12. const map2 = map1.set(true, 'yes');
  13. // map2 is a different object than map0,
  14. // but it has the same content
  15. assert.ok(map2 !== map0);
  16. assert.equal(map2.equals(map0), true); // (B)

Notes:

  • Instead of modifying the receiver, “destructive” operations such as .set() return modified copies.
  • To check if two data structures have the same content, we use the built-in .equals() method (line A and line B).

8.5.2 Immer

In its repository, the library Immer is described as:

Create the next immutable state by mutating the current one.

Immer helps with non-destructively updating (potentially nested) plain objects, Arrays, Sets, and Maps. That is, there are no custom data structures involved.

This is what using Immer looks like:

  1. import {produce} from 'immer/dist/immer.module.js';
  2. const people = [
  3. {name: 'Jane', work: {employer: 'Acme'}},
  4. ];
  5. const modifiedPeople = produce(people, (draft) => {
  6. draft[0].work.employer = 'Cyberdyne';
  7. draft.push({name: 'John', work: {employer: 'Spectre'}});
  8. });
  9. assert.deepEqual(modifiedPeople, [
  10. {name: 'Jane', work: {employer: 'Cyberdyne'}},
  11. {name: 'John', work: {employer: 'Spectre'}},
  12. ]);
  13. assert.deepEqual(people, [
  14. {name: 'Jane', work: {employer: 'Acme'}},
  15. ]);

The original data is stored in people. produce() provides us with a variable draft. We pretend that this variable is people and use operations with which we would normally make destructive changes. Immer intercepts these operations. Instead of mutating draft, it non-destructively changes people. The result is referenced by modifiedPeople. As a bonus, it is deeply immutable.

assert.deepEqual() works because Immer returns plain objects and Arrays.