diff --git a/CHANGELOG.md b/CHANGELOG.md
index 67b0153053a..75ffcc85daf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,11 @@
- Pretend that `__typename` exists on the root Query when matching fragments.
[@benjamn](https://github.com/benjamn) in [#4853](https://github.com/apollographql/apollo-client/pull/4853)
+### Apollo Utilities
+
+- The `isEqual` function has been reimplemented using the `lodash.isequal` npm package, to better support circular references. Since the `lodash.isequal` package is already used by `react-apollo`, this change is likely to decrease total bundle size.
+ [@capaj](https://github.com/capaj) in [#4915](https://github.com/apollographql/apollo-client/pull/4915)
+
## Apollo Client (2.6.0)
- In production, `invariant(condition, message)` failures will now include
diff --git a/packages/apollo-utilities/package-lock.json b/packages/apollo-utilities/package-lock.json
index 250091be6a6..002098f0350 100644
--- a/packages/apollo-utilities/package-lock.json
+++ b/packages/apollo-utilities/package-lock.json
@@ -1,14 +1,32 @@
{
"name": "apollo-utilities",
- "version": "1.2.1",
+ "version": "1.3.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
+ "@types/lodash": {
+ "version": "4.14.133",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.133.tgz",
+ "integrity": "sha512-/3JqnvPnY58GLzG3Y7fpphOhATV1DDZ/Ak3DQufjlRK5E4u+s0CfClfNFtAGBabw+jDGtRFbOZe+Z02ZMWCBNQ=="
+ },
+ "@types/lodash.isequal": {
+ "version": "4.5.5",
+ "resolved": "https://registry.npmjs.org/@types/lodash.isequal/-/lodash.isequal-4.5.5.tgz",
+ "integrity": "sha512-4IKbinG7MGP131wRfceK6W4E/Qt3qssEFLF30LnJbjYiSfHGGRU/Io8YxXrZX109ir+iDETC8hw8QsDijukUVg==",
+ "requires": {
+ "@types/lodash": "*"
+ }
+ },
"fast-json-stable-stringify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
"integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I="
},
+ "lodash.isequal": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+ "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
+ },
"ts-invariant": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.4.0.tgz",
diff --git a/packages/apollo-utilities/package.json b/packages/apollo-utilities/package.json
index f1563de4ff8..c69c15ec743 100644
--- a/packages/apollo-utilities/package.json
+++ b/packages/apollo-utilities/package.json
@@ -40,7 +40,11 @@
},
"dependencies": {
"fast-json-stable-stringify": "^2.0.0",
+ "lodash.isequal": "^4.5.0",
"ts-invariant": "^0.4.0",
"tslib": "^1.9.3"
+ },
+ "devDependencies": {
+ "@types/lodash.isequal": "^4.5.5"
}
}
diff --git a/packages/apollo-utilities/src/util/__tests__/isEqual.ts b/packages/apollo-utilities/src/util/__tests__/isEqual.ts
index 2865d43a038..e621a837082 100644
--- a/packages/apollo-utilities/src/util/__tests__/isEqual.ts
+++ b/packages/apollo-utilities/src/util/__tests__/isEqual.ts
@@ -90,4 +90,85 @@ describe('isEqual', () => {
expect(!isEqual([1, 2, 3], [1, 2, 4])).toBe(true);
delete Array.prototype.foo;
});
+
+ describe('comparing objects with circular refs', () => {
+ // copied with slight modification from lodash test suite
+ it('should compare objects with circular references', () => {
+ const object1 = {},
+ object2 = {};
+
+ object1.a = object1;
+ object2.a = object2;
+
+ expect(isEqual(object1, object2)).toBe(true);
+
+ object1.b = 0;
+ object2.b = Object(0);
+
+ expect(isEqual(object1, object2)).toBe(true);
+
+ object1.c = Object(1);
+ object2.c = Object(2);
+
+ expect(isEqual(object1, object2)).toBe(false);
+
+ object1 = { a: 1, b: 2, c: 3 };
+ object1.b = object1;
+ object2 = { a: 1, b: { a: 1, b: 2, c: 3 }, c: 3 };
+
+ expect(isEqual(object1, object2)).toBe(false);
+ });
+
+ it('should have transitive equivalence for circular references of objects', () => {
+ const object1 = {},
+ object2 = { a: object1 },
+ object3 = { a: object2 };
+
+ object1.a = object1;
+
+ expect(isEqual(object1, object2)).toBe(true);
+ expect(isEqual(object2, object3)).toBe(true);
+ expect(isEqual(object1, object3)).toBe(true);
+ });
+
+ it('should compare objects with multiple circular references', () => {
+ const array1 = [{}],
+ array2 = [{}];
+
+ (array1[0].a = array1).push(array1);
+ (array2[0].a = array2).push(array2);
+
+ expect(isEqual(array1, array2)).toBe(true);
+
+ array1[0].b = 0;
+ array2[0].b = Object(0);
+
+ expect(isEqual(array1, array2)).toBe(true);
+
+ array1[0].c = Object(1);
+ array2[0].c = Object(2);
+
+ expect(isEqual(array1, array2)).toBe(false);
+ });
+
+ it('should compare objects with complex circular references', () => {
+ const object1 = {
+ foo: { b: { c: { d: {} } } },
+ bar: { a: 2 },
+ };
+
+ const object2 = {
+ foo: { b: { c: { d: {} } } },
+ bar: { a: 2 },
+ };
+
+ object1.foo.b.c.d = object1;
+ object1.bar.b = object1.foo.b;
+
+ object2.foo.b.c.d = object2;
+ object2.bar.b = object2.foo.b;
+
+ expect(isEqual(object1, object2)).toBe(true);
+ });
+ });
});
diff --git a/packages/apollo-utilities/src/util/isEqual.ts b/packages/apollo-utilities/src/util/isEqual.ts
index 15bedc2696b..7713a31a082 100644
--- a/packages/apollo-utilities/src/util/isEqual.ts
+++ b/packages/apollo-utilities/src/util/isEqual.ts
@@ -1,47 +1,8 @@
+import isEqualLodash from 'lodash.isequal';
+
/**
* Performs a deep equality check on two JavaScript values.
*/
export function isEqual(a: any, b: any): boolean {
- // If the two values are strictly equal, we are good.
- if (a === b) {
- return true;
- }
- // Dates are equivalent if their time values are equal.
- if (a instanceof Date && b instanceof Date) {
- return a.getTime() === b.getTime();
- }
- // If a and b are both objects, we will compare their properties. This will compare arrays as
- // well.
- if (
- a != null &&
- typeof a === 'object' &&
- b != null &&
- typeof b === 'object'
- ) {
- // Compare all of the keys in `a`. If one of the keys has a different value, or that key does
- // not exist in `b` return false immediately.
- for (const key in a) {
- if (Object.prototype.hasOwnProperty.call(a, key)) {
- if (!Object.prototype.hasOwnProperty.call(b, key)) {
- return false;
- }
- if (!isEqual(a[key], b[key])) {
- return false;
- }
- }
- }
- // Look through all the keys in `b`. If `b` has a key that `a` does not, return false.
- for (const key in b) {
- if (
- Object.prototype.hasOwnProperty.call(b, key) &&
- !Object.prototype.hasOwnProperty.call(a, key)
- ) {
- return false;
- }
- }
- // If we made it this far the objects are equal!
- return true;
- }
- // Otherwise the values are not equal.
- return false;
+ return isEqualLodash(a, b);
}