This blog post answers the following questions:
Sections marked with “(advanced)” go deeper and can be skipped if you want to read this blog post more quickly.
Shared mutable state works as follows:
then there is a risk of one party’s modifications preventing other parties from working correctly. This is an example:
function logElements(arr) {
while (arr.length > 0) {
console.log(arr.shift());
}
}
function main() {
const arr = ['banana', 'orange', 'apple'];
console.log('Before sorting:');
logElements(arr);
arr.sort(); // changes arr
console.log('After sorting:');
logElements(arr); // (A)
}
main();
// Output:
// 'Before sorting:'
// 'banana'
// 'orange'
// 'apple'
// 'After sorting:'
Here, there are two independent parties: function logElements()
and function main()
. The latter wants to log an Array before and after sorting it. However, it uses logElements()
, which clears its parameter. Therefore, main()
logs an empty Array in line A.
In the remainder of this post, we look at three ways of avoiding the problems of shared mutable state:
In particular, we will come back to the example that we’ve just seen and fix it.
Before we can get into how copying avoids sharing, we need to take a look at how data can be copied in JavaScript.
There are two “depths” with which data can be copied:
The next sections cover both kinds of copying. Unfortunately, JavaScript only has built-in support for shallow copying. If we need deep copying, we need to implement it ourselves.
Let’s look at several ways of shallowly copying data.
We can spread into object literals and spread into Array literals to make copies:
const copyOfObject = {...originalObject};
const copyOfArray = [...originalArray];
Alas, spreading has several limitations:
The prototype is not copied:
class MyClass {}
const original = new MyClass();
assert.equal(MyClass.prototype.isPrototypeOf(original), true);
const copy = {...original};
assert.equal(MyClass.prototype.isPrototypeOf(copy), false);
Special objects such as regular expressions and dates have special “internal slots” that aren’t copied.
Only own (non-inherited) properties are copied. Given how prototype chains work, this is usually the best approach. But you still need to be aware of it. In the following example, the inherited property .inheritedProp
of original
is not available in copy
, because we only copy own properties and don’t keep the prototype.
const proto = { inheritedProp: 'a' };
const original = {__proto__: proto, ownProp: 'b' };
assert.equal(original.inheritedProp, 'a');
assert.equal(original.ownProp, 'b');
const copy = {...original};
assert.equal(copy.inheritedProp, undefined);
assert.equal(copy.ownProp, 'b');
Only enumerable properties are copied. For example, the own property .length
of Array instances is not enumerable and not copied:
const arr = ['a', 'b'];
assert.equal(arr.length, 2);
assert.equal({}.hasOwnProperty.call(arr, 'length'), true);
const copy = {...arr};
assert.equal({}.hasOwnProperty.call(copy, 'length'), false);
Independently of the attributes of a property, its copy will always be a data property that is writable and configurable – for example:
const original = Object.defineProperties({}, {
prop: {
value: 1,
writable: false,
configurable: false,
enumerable: true,
},
});
assert.deepEqual(original, {prop: 1});
const copy = {...original};
// Attributes `writable` and `configurable` of copy are different:
assert.deepEqual(Object.getOwnPropertyDescriptors(copy), {
prop: {
value: 1,
writable: true,
configurable: true,
enumerable: true,
},
});
That means that getters and setters are not copied faithfully, either: The attributes value
(for data properties), get
(for getters), and set
(for setters) are mutually exclusive.
const original = {
get myGetter() { return 123 },
set mySetter(x) {},
};
assert.deepEqual({...original}, {
myGetter: 123, // not a getter anymore!
mySetter: undefined,
});
Copying is shallow: The copy has fresh versions of each key-value entry in the original, but the values of the original are not copied themselves. For example:
const original = {name: 'Jane', work: {employer: 'Acme'}};
const copy = {...original};
// Property .name is a copy
copy.name = 'John';
assert.deepEqual(original,
{name: 'Jane', work: {employer: 'Acme'}});
assert.deepEqual(copy,
{name: 'John', work: {employer: 'Acme'}});
// The value of .work is shared
copy.work.employer = 'Spectre';
assert.deepEqual(
original, {name: 'Jane', work: {employer: 'Spectre'}});
assert.deepEqual(
copy, {name: 'John', work: {employer: 'Spectre'}});
Some of these limitations can be eliminated, others can’t:
We can give the copy the same prototype as the original during copying:
class MyClass {}
const original = new MyClass();
const copy = {
__proto__: Object.getPrototypeOf(original),
...original,
};
assert.equal(MyClass.prototype.isPrototypeOf(copy), true);
Alternatively, we can set the prototype of the copy after its creation, via Object.setPrototypeOf()
.
There is no simple way to generically copy special objects.
As mentioned, only own properties being copied is more of a feature than a limitation.
We can use Object.getOwnPropertyDescriptors()
and Object.defineProperties()
to copy objects (how to do that is explained later):
value
) and therefore correctly copy getters, setters, read-only properties, etc.Object.getOwnPropertyDescriptors()
retrieves both enumerable and non-enumerable properties.We’ll look at deep copying later in this post.
Object.assign()
(advanced) Object.assign()
works mostly like spreading into objects. That is, the following two ways of copying are mostly equivalent:
const copy1 = {...original};
const copy2 = Object.assign({}, original);
Using a method instead of syntax has the benefit that it can be polyfilled on older JavaScript engines via a library.
Object.assign()
is not completely like spreading, though. It differs in one, relatively subtle point: it creates properties differently.
Object.assign()
uses assignment to create the properties of the copy.Among other things, assignment invokes own and inherited setters, while definition doesn’t (more information on assignment vs. definition). This difference is rarely noticeable. The following code is an example, but it’s contrived:
const original = {['__proto__']: null};
const copy1 = {...original};
// copy1 has the own property '__proto__'
assert.deepEqual(
Object.keys(copy1), ['__proto__']);
const copy2 = Object.assign({}, original);
// copy2 has the prototype null
assert.equal(Object.getPrototypeOf(copy2), null);
Object.getOwnPropertyDescriptors()
and Object.defineProperties()
(advanced) JavaScript lets us create properties via property descriptors, objects that specify property attributes. For example, via the Object.defineProperties()
, which we have already seen in action. If we combine that method with Object.getOwnPropertyDescriptors()
, we can copy more faithfully:
function copyAllOwnProperties(original) {
return Object.defineProperties(
{}, Object.getOwnPropertyDescriptors(original));
}
That eliminates two limitations of copying objects via spreading.
First, all attributes of own properties are copied correctly. Therefore, we can now copy own getters and own setters:
const original = {
get myGetter() { return 123 },
set mySetter(x) {},
};
assert.deepEqual(copyAllOwnProperties(original), original);
Second, thanks to Object.getOwnPropertyDescriptors()
, non-enumerable properties are copied, too:
const arr = ['a', 'b'];
assert.equal(arr.length, 2);
assert.equal({}.hasOwnProperty.call(arr, 'length'), true);
const copy = copyAllOwnProperties(arr);
assert.equal({}.hasOwnProperty.call(copy, 'length'), true);
Now it is time to tackle deep copying. First, we will deep-copy manually, then we’ll examine generic approaches.
If we nest spreading, we get deep copies:
const original = {name: 'Jane', work: {employer: 'Acme'}};
const copy = {name: original.name, work: {...original.work}};
// We copied successfully:
assert.deepEqual(original, copy);
// The copy is deep:
assert.ok(original.work !== copy.work);
This is a hack, but, in a pinch, it provides a quick solution: In order to deep-copy an object original
, we first convert it to a JSON string and the parse that JSON string:
function jsonDeepCopy(original) {
return JSON.parse(JSON.stringify(original));
}
const original = {name: 'Jane', work: {employer: 'Acme'}};
const copy = jsonDeepCopy(original);
assert.deepEqual(original, copy);
The significant downside of this approach is that we can only copy properties with keys and values that are supported by JSON.
Some unsupported keys and values are simply ignored:
assert.deepEqual(
jsonDeepCopy({
[Symbol('a')]: 'abc',
b: function () {},
c: undefined,
}),
{} // empty object
);
Others cause exceptions:
assert.throws(
() => jsonDeepCopy({a: 123n}),
/^TypeError: Do not know how to serialize a BigInt$/);
The following function generically deep-copies a value original
:
function deepCopy(original) {
if (Array.isArray(original)) {
const copy = [];
for (const [index, value] of original.entries()) {
copy[index] = deepCopy(value);
}
return copy;
} else if (typeof original === 'object' && original !== null) {
const copy = {};
for (const [key, value] of Object.entries(original)) {
copy[key] = deepCopy(value);
}
return copy;
} else {
// Primitive value: atomic, no need to copy
return original;
}
}
The function handles three cases:
original
is an Array we create a new Array and deep-copy the elements of original
into it.original
is an object, we use a similar approach.original
is a primitive value, we don’t have to do anything.Let’s try out deepCopy()
:
const original = {a: 1, b: {c: 2, d: {e: 3}}};
const copy = deepCopy(original);
// Are copy and original deeply equal?
assert.deepEqual(copy, original);
// Did we really copy all levels
// (equal content, but different objects)?
assert.ok(copy !== original);
assert.ok(copy.b !== original.b);
assert.ok(copy.b.d !== original.b.d);
Note that deepCopy()
only fixes one issue of spreading: shallow copying. All others remain: prototypes are not copied, special objects are only partially copied, non-enumerable properties are ignored, most property attributes are ignored.
Implementing copying completely generically is generally impossible: Not all data is a tree, sometimes you don’t want to all properties, etc.
deepCopy()
We can make our previous implementation of deepCopy()
more concise if we use .map()
and Object.fromEntries()
:
function deepCopy(original) {
if (Array.isArray(original)) {
return original.map(elem => deepCopy(elem));
} else if (typeof original === 'object' && original !== null) {
return Object.fromEntries(
Object.entries(original)
.map(([k, v]) => [k, deepCopy(v)]));
} else {
// Primitive value: atomic, no need to copy
return original;
}
}
Two techniques are often used to implement deep copying for instances of classes:
.clone()
methods.clone()
methods This technique introduces one method .clone()
per class whose instances are to be deep-copied. It returns a deep copy of this
. The following example shows three classes that can be cloned.
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
clone() {
return new Point(this.x, this.y);
}
}
class Color {
constructor(name) {
this.name = name;
}
clone() {
return new Color(this.name);
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y);
this.color = color;
}
clone() {
return new ColorPoint(
this.x, this.y, this.color.clone()); // (A)
}
}
Line A demonstrates an important aspect of this technique: compound instance property values must also be cloned, recursively.
A copy constructor is a constructor that uses another instance of the current class to set up the current instance. Copy constructors are popular in static languages such as C++ and Java, where you can provide multiple versions of a constructor via static overloading (static meaning that it happens at compile time).
In JavaScript, you could do something like this (but it’s not very elegant):
class Point {
constructor(...args) {
if (args[0] instanceof Point) {
// Copy constructor
const [other] = args;
this.x = other.x;
this.y = other.y;
} else {
const [x, y] = args;
this.x = x;
this.y = y;
}
}
}
This is how you’d use this class:
const original = new Point(-1, 4);
const copy = new Point(original);
assert.deepEqual(copy, original);
Instead, static factory methods work better in JavaScript (static meaning that they are class methods).
In the following example, the three classes Point
, Color
and ColorPoint
each have a static factory method .from()
:
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
static from(other) {
return new Point(other.x, other.y);
}
}
class Color {
constructor(name) {
this.name = name;
}
static from(other) {
return new Color(other.name);
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y);
this.color = color;
}
static from(other) {
return new ColorPoint(
other.x, other.y, Color.from(other.color)); // (A)
}
}
In line A, we once again use recursive copying.
This is how ColorPoint.from()
works:
const original = new ColorPoint(-1, 4, new Color('red'));
const copy = ColorPoint.from(original);
assert.deepEqual(copy, original);
As long as we only read from shared state, we don’t have any problems. Before we modify it, we need to “un-share” it, by copying it (as deeply as necessary).
Defensive copying is a technique to always copy when issues might arise. Its objective is to keep the current entity (function, class, etc.) safe:
Note that these measures protect us from other parties, but they also protect other parties from us.
The next sections illustrate both kinds of defensive copying.
Remember that in the motivating example at the beginning of this post, we got into trouble because logElements()
modified its parameter arr
:
function logElements(arr) {
while (arr.length > 0) {
console.log(arr.shift());
}
}
Let’s add defensive copying to this function:
function logElements(arr) {
arr = [...arr]; // defensive copy
while (arr.length > 0) {
console.log(arr.shift());
}
}
Now logElements()
doesn’t cause problems anymore, if it is called inside main()
:
function main() {
const arr = ['banana', 'orange', 'apple'];
console.log('Before sorting:');
logElements(arr);
arr.sort(); // changes arr
console.log('After sorting:');
logElements(arr); // (A)
}
main();
// Output:
// 'Before sorting:'
// 'banana'
// 'orange'
// 'apple'
// 'After sorting:'
// 'apple'
// 'banana'
// 'orange'
Let’s start with a class StringBuilder
that doesn’t copy internal data it exposes (line A):
class StringBuilder {
constructor() {
this._data = [];
}
add(str) {
this._data.push(str);
}
getParts() {
// We expose internals without copying them:
return this._data; // (A)
}
toString() {
return this._data.join('');
}
}
As long as .getParts()
isn’t used, everything works well:
const sb1 = new StringBuilder();
sb1.add('Hello');
sb1.add(' world!');
assert.equal(sb1.toString(), 'Hello world!');
If, however, the result of .getParts()
is changed (line A), then the StringBuilder
ceases to work correctly:
const sb2 = new StringBuilder();
sb2.add('Hello');
sb2.add(' world!');
sb2.getParts().length = 0; // (A)
assert.equal(sb2.toString(), ''); // not OK
The solution is to copy the internal ._data
defensively before it is exposed (line A):
class StringBuilder {
constructor() {
this._data = [];
}
add(str) {
this._data.push(str);
}
getParts() {
// Copy defensively
return [...this._data]; // (A)
}
toString() {
return this._data.join('');
}
}
Now changing the result of .getParts()
doesn’t interfere with the operation of sb
anymore:
const sb = new StringBuilder();
sb.add('Hello');
sb.add(' world!');
sb.getParts().length = 0;
assert.equal(sb.toString(), 'Hello world!'); // OK
We will first explore the difference between updating data destructively and non-destructively. Then we’ll learn how non-destructive updates avoid mutations.
We can distinguish two different ways of updating data:
The latter way is similar to first making a copy and then changing it destructively, but it does both at the same time.
This is how we destructively set the property .city
of an object:
const obj = {city: 'Berlin', country: 'Germany'};
const key = 'city';
obj[key] = 'Munich';
assert.deepEqual(obj, {city: 'Munich', country: 'Germany'});
The following function non-destructively changes properties:
function setObjectNonDestructively(obj, key, value) {
const updatedObj = {};
for (const [k, v] of Object.entries(obj)) {
updatedObj[k] = (k === key ? value : v);
}
return updatedObj;
}
It is used as follows:
const obj = {city: 'Berlin', country: 'Germany'};
const updatedObj = setObjectNonDestructively(obj, 'city', 'Munich');
assert.deepEqual(updatedObj, {city: 'Munich', country: 'Germany'});
assert.deepEqual(obj, {city: 'Berlin', country: 'Germany'});
Spreading makes setObjectNonDestructively()
more concise:
function setObjectNonDestructively(obj, key, value) {
return {...obj, [key]: value};
}
Note: Both versions of setObjectNonDestructively()
update shallowly.
This is how we destructively set an element of an Array:
const original = ['a', 'b', 'c', 'd', 'e'];
original[2] = 'x';
assert.deepEqual(original, ['a', 'b', 'x', 'd', 'e']);
Non-destructively updating an Array is more complicated than non-destructively updating an object.
function setArrayNonDestructively(arr, index, value) {
const updatedArr = [];
for (const [i, v] of arr.entries()) {
updatedArr.push(i === index ? value : v);
}
return updatedArr;
}
const arr = ['a', 'b', 'c', 'd', 'e'];
const updatedArr = setArrayNonDestructively(arr, 2, 'x');
assert.deepEqual(updatedArr, ['a', 'b', 'x', 'd', 'e']);
assert.deepEqual(arr, ['a', 'b', 'c', 'd', 'e']);
.slice()
and spreading make setArrayNonDestructively()
more concise:
function setArrayNonDestructively(arr, index, value) {
return [
...arr.slice(0, index), value, ...arr.slice(index+1)]
}
Note: Both versions of setArrayNonDestructively()
update shallowly.
So far, we have only updated data shallowly. Let’s tackle deep updating. The following code shows how to do it manually. We are changing name and employer.
const original = {name: 'Jane', work: {employer: 'Acme'}};
const updatedOriginal = {
...original,
name: 'John',
work: {
...original.work,
employer: 'Spectre'
},
};
assert.deepEqual(
original, {name: 'Jane', work: {employer: 'Acme'}});
assert.deepEqual(
updatedOriginal, {name: 'John', work: {employer: 'Spectre'}});
The following function implements generic deep updating.
function deepUpdate(original, keys, value) {
if (keys.length === 0) {
return value;
}
const currentKey = keys[0];
if (Array.isArray(original)) {
return original.map(
(v, index) => index === currentKey
? deepUpdate(v, keys.slice(1), value) // (A)
: v); // (B)
} else if (typeof original === 'object' && original !== null) {
return Object.fromEntries(
Object.entries(original).map(
(keyValuePair) => {
const [k,v] = keyValuePair;
if (k === currentKey) {
return [k, deepUpdate(v, keys.slice(1), value)]; // (C)
} else {
return keyValuePair; // (D)
}
}));
} else {
// Primitive value
return original;
}
}
If we see value
as the root of a tree that we are updating, then deepUpdate()
only deeply changes a single branch (line A and C). All other branches are copied shallowly (line B and D).
This is what using deepUpdate()
looks like:
const original = {name: 'Jane', work: {employer: 'Acme'}};
const copy = deepUpdate(original, ['work', 'employer'], 'Spectre');
assert.deepEqual(copy, {name: 'Jane', work: {employer: 'Spectre'}});
assert.deepEqual(original, {name: 'Jane', work: {employer: 'Acme'}});
With non-destructive updating, sharing data becomes unproblematic, because we never mutate the shared data. (Obviously, this only works if all parties do this.)
Intriguingly, copying data becomes trivially simple:
const original = {city: 'Berlin', country: 'Germany'};
const copy = original;
The actual copying of original
happens only if and when it is necessary and we are making non-destructive changes.
We can prevent mutations of shared data by making that data immutable. Next, we’ll examine how JavaScript supports immutability. Afterwards, we’ll discuss how immutable data helps with shared mutable state.
JavaScript has three levels of protecting objects:
Object.preventExtensions(obj)
Object.seal(obj)
Object.freeze(obj)
For more information, see “Speaking JavaScript”.
Given that we want our objects to be completely immutable, we only use Object.freeze()
in this blog post.
Object.freeze(obj)
only freezes obj
and its properties. It does not freeze the values of those properties – for example:
const teacher = {
name: 'Edna Krabappel',
students: ['Bart'],
};
Object.freeze(teacher);
assert.throws(
() => teacher.name = 'Elizabeth Hoover',
/^TypeError: Cannot assign to read only property 'name'/);
teacher.students.push('Lisa');
assert.deepEqual(
teacher, {
name: 'Edna Krabappel',
students: ['Bart', 'Lisa'],
});
If we want deep freezing, we need to implement it ourselves:
function deepFreeze(value) {
if (Array.isArray(value)) {
for (const element of value) {
deepFreeze(element);
}
Object.freeze(value);
} else if (typeof value === 'object' && value !== null) {
for (const v of Object.values(value)) {
deepFreeze(v);
}
Object.freeze(value);
} else {
// Nothing to do: primitive values are already immutable
}
return value;
}
Revisiting the example from the previous section, we can check if deepFreeze()
really freezes deeply:
const teacher = {
name: 'Edna Krabappel',
students: ['Bart'],
};
deepFreeze(teacher);
assert.throws(
() => teacher.name = 'Elizabeth Hoover',
/^TypeError: Cannot assign to read only property 'name'/);
assert.throws(
() => teacher.students.push('Lisa'),
/^TypeError: Cannot add property 1, object is not extensible$/);
An immutable wrapper wraps a mutable collection and provides the same API, but without destructive operations. We now have two interfaces for the same collection: One is mutable, the other one is immutable. This is useful when we have mutable internal data that we want to expose safely.
The next two sections showcase wrappers for Maps and Arrays. They both have the following limitations:
Class ImmutableMapWrapper
produces wrappers for Maps:
class ImmutableMapWrapper {
constructor(map) {
this._self = map;
}
}
// Only forward non-destructive methods to the wrapped Map:
for (const methodName of ['get', 'has', 'keys', 'size']) {
ImmutableMapWrapper.prototype[methodName] = function (...args) {
return this._self[methodName](...args);
}
}
This is the class in action:
const map = new Map([[false, 'no'], [true, 'yes']]);
const wrapped = new ImmutableMapWrapper(map);
// Non-destructive operations work as usual:
assert.equal(
wrapped.get(true), 'yes');
assert.equal(
wrapped.has(false), true);
assert.deepEqual(
[...wrapped.keys()], [false, true]);
// Destructive operations are not available:
assert.throws(
() => wrapped.set(false, 'never!'),
/^TypeError: wrapped.set is not a function$/);
assert.throws(
() => wrapped.clear(),
/^TypeError: wrapped.clear is not a function$/);
For an Array arr
, normal wrapping is not enough because we need to intercept not just method calls, but also property accesses such as arr[1] = true
. JavaScript proxies enable us to do this:
const RE_INDEX_PROP_KEY = /^[0-9]+$/;
const ALLOWED_PROPERTIES = new Set([
'length', 'constructor', 'slice', 'concat']);
function wrapArrayImmutably(arr) {
const handler = {
get(target, propKey, receiver) {
// We assume that propKey is a string (not a symbol)
if (RE_INDEX_PROP_KEY.test(propKey) // simplified check!
|| ALLOWED_PROPERTIES.has(propKey)) {
return Reflect.get(target, propKey, receiver);
}
throw new TypeError(`Property "${propKey}" can’t be accessed`);
},
set(target, propKey, value, receiver) {
throw new TypeError('Setting is not allowed');
},
deleteProperty(target, propKey) {
throw new TypeError('Deleting is not allowed');
},
};
return new Proxy(arr, handler);
}
Let’s wrap an Array:
const arr = ['a', 'b', 'c'];
const wrapped = wrapArrayImmutably(arr);
// Non-destructive operations are allowed:
assert.deepEqual(
wrapped.slice(1), ['b', 'c']);
assert.equal(
wrapped[1], 'b');
// Destructive operations are not allowed:
assert.throws(
() => wrapped[1] = 'x',
/^TypeError: Setting is not allowed$/);
assert.throws(
() => wrapped.shift(),
/^TypeError: Property "shift" can’t be accessed$/);
If data is immutable, it can be shared without any risks. In particular, there is no need to copy defensively.
Non-destructive updating complements immutable data and makes it mostly as versatile as mutable data, but without the associated risks.
There are several libraries available for JavaScript that support immutable data with non-destructive updating. Two popular ones are:
List
, Map
, Set
, and Stack
.These libraries are described in more detail in the next two sections.
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
Map
(which is different from JavaScript’s built-in Map
)Set
(which is different from JavaScript’s built-in Set
)Stack
In the following example, we use an immutable Map
:
import {Map} from 'immutable/dist/immutable.es.js';
const map0 = Map([
[false, 'no'],
[true, 'yes'],
]);
const map1 = map0.set(true, 'maybe'); // (A)
assert.ok(map1 !== map0); // (B)
assert.equal(map1.equals(map0), false);
const map2 = map1.set(true, 'yes'); // (C)
assert.ok(map2 !== map1);
assert.ok(map2 !== map0);
assert.equal(map2.equals(map0), true); // (D)
Explanations:
map1
of map0
, where true
is mapped to 'maybe'
.map1
and undo the change made in line A..equals()
method to check that we really undid the change.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 and Arrays. That is, there are no special data structures involved.
This is what using Immer looks like:
import {produce} from 'immer/dist/immer.module.js';
const people = [
{name: 'Jane', work: {employer: 'Acme'}},
];
const modifiedPeople = produce(people, (draft) => {
draft[0].work.employer = 'Cyberdyne';
draft.push({name: 'John', work: {employer: 'Spectre'}});
});
assert.deepEqual(modifiedPeople, [
{name: 'Jane', work: {employer: 'Cyberdyne'}},
{name: 'John', work: {employer: 'Spectre'}},
]);
assert.deepEqual(people, [
{name: 'Jane', work: {employer: 'Acme'}},
]);
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.