Skip to content

Commit 6171d78

Browse files
committed
preserve referential equality when possible
1 parent 0c57a71 commit 6171d78

File tree

4 files changed

+116
-43
lines changed

4 files changed

+116
-43
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ const withTax = update(state, {
3131
});
3232
assert(JSON.stringify(withTax) === JSON.stringify({ price: 221.4 });
3333
```
34+
35+
Note that `original` in the function above is the original object, so if you plan making a
36+
mutation, you must first shallow clone the object. Another option is to
37+
use `update` to make the change `return update(original, { foo: {$set: 'bar'} })`
38+
3439
If you don't want to mess around with the globally exported `update` function you can make a copy and work with that copy:
3540
3641
```js

index.js

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,21 @@ function newContext() {
4040
'$set'
4141
);
4242

43-
var newObject = copy(object);
43+
var newObject = object;
4444
for (var key in spec) {
4545
if (hasOwnProperty.call(commands, key)) {
46-
return commands[key](spec[key], newObject, spec);
46+
return commands[key](spec[key], newObject, spec, object);
4747
}
4848
}
4949
for (var key in spec) {
50-
newObject[key] = update(object[key], spec[key]);
50+
var nextValueForKey = update(object[key], spec[key]);
51+
if (nextValueForKey === object[key]) {
52+
continue;
53+
}
54+
if (newObject === object) {
55+
newObject = copy(object);
56+
}
57+
newObject[key] = nextValueForKey;
5158
}
5259
return newObject;
5360
}
@@ -63,24 +70,26 @@ var defaultCommands = {
6370
invariantPushAndUnshift(original, spec, '$unshift');
6471
return value.concat(original);
6572
},
66-
$splice: function(value, original, spec) {
67-
invariantSplices(original, spec);
73+
$splice: function(value, newObject, spec, object) {
74+
var originalValue = newObject === object ? copy(object) : newObject;
75+
invariantSplices(originalValue, spec);
6876
value.forEach(function(args) {
6977
invariantSplice(args);
70-
splice.apply(original, args);
78+
splice.apply(originalValue, args);
7179
});
72-
return original;
80+
return originalValue;
7381
},
7482
$set: function(value, original, spec) {
7583
invariantSet(spec);
76-
return value
84+
return value;
7785
},
78-
$merge: function(value, original, spec) {
79-
invariantMerge(original, value);
86+
$merge: function(value, newObject, spec, object) {
87+
var originalValue = newObject === object ? copy(object) : newObject;
88+
invariantMerge(originalValue, value);
8089
Object.keys(value).forEach(function(key) {
81-
original[key] = value[key];
90+
originalValue[key] = value[key];
8291
});
83-
return original;
92+
return originalValue;
8493
},
8594
$apply: function(value, original) {
8695
invariantApply(value);

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "immutability-helper",
3-
"version": "1.0.0",
3+
"version": "2.0.0",
44
"description": "mutate a copy of data without changing the original source",
55
"main": "index.js",
66
"scripts": {

test.js

Lines changed: 89 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,11 @@ describe('update', function() {
103103
update(obj, {$set: {c: 'd'}});
104104
expect(obj).toEqual({a: 'b'});
105105
});
106+
it('keeps reference equality when possible', function() {
107+
var original = {a: 1};
108+
expect(update(original, {a: {$set: 1}})).toBe(original);
109+
expect(update(original, {a: {$set: 2}})).toNotBe(original);
110+
});
106111
});
107112

108113
describe('$apply', function() {
@@ -122,38 +127,82 @@ describe('update', function() {
122127
'update(): expected spec of $apply to be a function; got 123.'
123128
);
124129
});
130+
it('keeps reference equality when possible', function() {
131+
var original = {a: {b: {}}};
132+
function identity(val) {
133+
return val;
134+
}
135+
expect(update(original, {a: {$apply: identity}})).toBe(original);
136+
expect(update(original, {a: {$apply: applier}})).toNotBe(original);
137+
});
125138
});
126139

127-
it('should support deep updates', function() {
128-
expect(update({
129-
a: 'b',
130-
c: {
131-
d: 'e',
132-
f: [1],
133-
g: [2],
134-
h: [3],
135-
i: {j: 'k'},
136-
l: 4,
137-
},
138-
}, {
139-
c: {
140-
d: {$set: 'm'},
141-
f: {$push: [5]},
142-
g: {$unshift: [6]},
143-
h: {$splice: [[0, 1, 7]]},
144-
i: {$merge: {n: 'o'}},
145-
l: {$apply: function(x) { return x * 2 } },
146-
},
147-
})).toEqual({
148-
a: 'b',
149-
c: {
150-
d: 'm',
151-
f: [1, 5],
152-
g: [6, 2],
153-
h: [7],
154-
i: {j: 'k', n: 'o'},
155-
l: 8,
156-
},
140+
describe('deep update', function() {
141+
it('works', function() {
142+
expect(update({
143+
a: 'b',
144+
c: {
145+
d: 'e',
146+
f: [1],
147+
g: [2],
148+
h: [3],
149+
i: {j: 'k'},
150+
l: 4,
151+
},
152+
}, {
153+
c: {
154+
d: {$set: 'm'},
155+
f: {$push: [5]},
156+
g: {$unshift: [6]},
157+
h: {$splice: [[0, 1, 7]]},
158+
i: {$merge: {n: 'o'}},
159+
l: {$apply: function(x) { return x * 2; }},
160+
},
161+
})).toEqual({
162+
a: 'b',
163+
c: {
164+
d: 'm',
165+
f: [1, 5],
166+
g: [6, 2],
167+
h: [7],
168+
i: {j: 'k', n: 'o'},
169+
l: 8,
170+
},
171+
});
172+
});
173+
it('keeps reference equality when possible', function() {
174+
var original = {a: {b: 1}, c: {d: {e: 1}}};
175+
176+
expect(update(original, {a: {b: {$set: 1}}})).toBe(original);
177+
expect(update(original, {a: {b: {$set: 1}}}).a).toBe(original.a);
178+
179+
expect(update(original, {c: {d: {e: {$set: 1}}}})).toBe(original);
180+
expect(update(original, {c: {d: {e: {$set: 1}}}}).c).toBe(original.c);
181+
expect(update(original, {c: {d: {e: {$set: 1}}}}).c.d).toBe(original.c.d);
182+
183+
expect(update(original, {
184+
a: {b: {$set: 1}},
185+
c: {d: {e: {$set: 1}}},
186+
})).toBe(original);
187+
expect(update(original, {
188+
a: {b: {$set: 1}},
189+
c: {d: {e: {$set: 1}}},
190+
}).a).toBe(original.a);
191+
expect(update(original, {
192+
a: {b: {$set: 1}},
193+
c: {d: {e: {$set: 1}}},
194+
}).c).toBe(original.c);
195+
expect(update(original, {
196+
a: {b: {$set: 1}},
197+
c: {d: {e: {$set: 1}}},
198+
}).c.d).toBe(original.c.d);
199+
200+
expect(update(original, {a: {b: {$set: 2}}})).toNotBe(original);
201+
expect(update(original, {a: {b: {$set: 2}}}).a).toNotBe(original.a);
202+
expect(update(original, {a: {b: {$set: 2}}}).a.b).toNotBe(original.a.b);
203+
204+
expect(update(original, {a: {b: {$set: 2}}}).c).toBe(original.c);
205+
expect(update(original, {a: {b: {$set: 2}}}).c.d).toBe(original.c.d);
157206
});
158207
});
159208

@@ -188,6 +237,16 @@ describe('update', function() {
188237
expect(myUpdate(5, {$addtax: 0.10})).toEqual(5.5);
189238
});
190239

240+
it('gets the original object (so be careful about mutations)', function() {
241+
var obj = {};
242+
var passedOriginal;
243+
myUpdate.extend('$foobar', function(prop, original) {
244+
passedOriginal = original;
245+
});
246+
myUpdate(obj, {$foobar: null});
247+
expect(obj).toBe(passedOriginal);
248+
});
249+
191250
it("doesn't touch the original update", function() {
192251
myUpdate.extend('$addtax', function(tax, original) {
193252
return original + (tax * original);

0 commit comments

Comments
 (0)