Skip to content

Commit 9230196

Browse files
authored
Merge pull request #75 from HubSpot/adam/handle-immutable-compares
fix(useStoreDependency): use shallowCompare to compare results
2 parents 96425f6 + 706543e commit 9230196

File tree

7 files changed

+82
-3
lines changed

7 files changed

+82
-3
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
## 3.2.0
2+
* Fix potential infinite loop with Immutable dependency values (#74)
3+
14
## 3.1.0
25
* Use `UNSAFE_` prefix for `componentWillMount`, `componentWillReceiveProps` usages in `connect` HOC.
36

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "general-store",
3-
"version": "3.1.0",
3+
"version": "3.2.0",
44
"description": "Simple, flexible store implementation for Flux.",
55
"main": "lib/GeneralStore.js",
66
"scripts": {
@@ -59,6 +59,7 @@
5959
"eslint-plugin-react-app": "^4.0.1",
6060
"eslint-plugin-react-hooks": "^1.5.1",
6161
"flux": "^2.0.1",
62+
"immutable": "3.8.1",
6263
"immutable-is": "^3.7.4",
6364
"invariant": "^2.2.1",
6465
"jest": "24.5.0",

src/dependencies/__tests__/useStoreDependency-test.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import useStoreDependency from '../useStoreDependency';
77
import { Dispatcher } from 'flux';
88
import { set as setDispatcherInstance } from '../../dispatcher/DispatcherInstance';
99
import TestUtils from 'react-dom/test-utils';
10+
import { List, Map } from 'immutable';
1011

1112
configure({ adapter: new Adapter() });
1213

@@ -159,6 +160,30 @@ describe('useStoreDependency', () => {
159160
rendered.update();
160161
expect(renders).toBe(2);
161162
});
163+
164+
it("doesn't trigger an infinite loop when using immutable objects", () => {
165+
const store = new StoreFactory({
166+
getter: state => state,
167+
getInitialState: () => List([Map({ a: 1 })]),
168+
responses: {
169+
updateImmutable: (state, newValue) => newValue,
170+
},
171+
}).register();
172+
const Component = () => {
173+
useStoreDependency({
174+
stores: [store],
175+
deref: () => List([Map({ a: 5 })]),
176+
});
177+
178+
return null;
179+
};
180+
mount(<Component />);
181+
// no assertions to make, but this test will fail if equality is
182+
// not implemented correctly, as the immutables will not be strictly
183+
// equal, sending useStoreDependency into an infinite loop
184+
// https://github.com/HubSpot/general-store/issues/74
185+
expect(true).toBe(true);
186+
});
162187
});
163188

164189
describe('with props', () => {

src/dependencies/useStoreDependency.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { get as getDispatcherInstance } from '../dispatcher/DispatcherInstance';
1010
import { enforceDispatcher } from '../dispatcher/DispatcherInterface';
1111
import { handleDispatch } from './Dispatch';
1212
import { Dispatcher } from 'flux';
13+
import { shallowEqual } from '../utils/ObjectUtils';
1314

1415
type SingleDependency = {
1516
[key: string]: Dependency;
@@ -50,7 +51,7 @@ function useStoreDependency<Props>(
5051
entry,
5152
currProps.current
5253
);
53-
if (newValue !== dependencyValue) {
54+
if (!shallowEqual(newValue, dependencyValue)) {
5455
setDependencyValue(newValue);
5556
}
5657
}
@@ -62,7 +63,7 @@ function useStoreDependency<Props>(
6263
}, [dispatcher, dependencyValue, dependency, currProps]);
6364

6465
const newValue = calculate(dependency, props);
65-
if (newValue !== dependencyValue) {
66+
if (!shallowEqual(newValue, dependencyValue)) {
6667
setDependencyValue(newValue);
6768
}
6869
return dependencyValue;

src/utils/ObjectUtils.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,27 @@ export function shallowEqual(obj1: any, obj2: any): Boolean {
6363
if (obj1 === obj2) {
6464
return true;
6565
}
66+
67+
// Special handling for Immutables, as they must be handled specially.
68+
// While this technically means this is deep equality for Immutables,
69+
// it's better to have a more specific check than an entirely incorrect one.
70+
if (
71+
typeof obj1.hashCode === 'function' &&
72+
typeof obj2.hashCode === 'function'
73+
) {
74+
// `hashCode` is guaranteed to be the same if the objects are the same, but
75+
// is NOT guaranteed to be different if the objects are different (hash
76+
// collisions are possible). If the hash codes are different, then we can
77+
// preemptively return false as a performance optimization. Otherwise,
78+
// return the result of a call to `equals`.
79+
// https://github.com/immutable-js/immutable-js/blob/59c291a2b37693198a0950637c5d55cd14dd6bc4/src/is.js#L52-L55
80+
if (obj1.hashCode() !== obj2.hashCode()) {
81+
return false;
82+
} else if (typeof obj1.equals === 'function') {
83+
return obj1.equals(obj2);
84+
}
85+
}
86+
6687
if (typeof obj1 !== typeof obj2) {
6788
return false;
6889
}

src/utils/__tests__/ObjectUtils-test.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
jest.unmock('../ObjectUtils');
22

3+
import { Map } from 'immutable';
34
import {
45
oForEach,
56
oFilterMap,
@@ -86,5 +87,27 @@ describe('ObjectUtils', () => {
8687
false
8788
);
8889
});
90+
91+
it('shallowly compares immutable values', () => {
92+
expect(shallowEqual(Map({ a: 1 }), Map({ a: 1 }))).toBe(true);
93+
const mockImmutable1 = {
94+
hashCode() {
95+
return 1;
96+
},
97+
equals() {
98+
return false;
99+
},
100+
};
101+
const mockImmutable2 = {
102+
hashCode() {
103+
return 1;
104+
},
105+
equals() {
106+
return false;
107+
},
108+
};
109+
// force hash collision - should fall back to .equals
110+
expect(shallowEqual(mockImmutable1, mockImmutable2)).toBe(false);
111+
});
89112
});
90113
});

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2371,6 +2371,11 @@ immutable-is@^3.7.4:
23712371
version "3.7.6"
23722372
resolved "https://registry.yarnpkg.com/immutable-is/-/immutable-is-3.7.6.tgz#efad8bcf21443392402e10fa08b7aa91b65f6c30"
23732373

2374+
immutable@3.8.1:
2375+
version "3.8.1"
2376+
resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.1.tgz#200807f11ab0f72710ea485542de088075f68cd2"
2377+
integrity sha1-IAgH8Rqw9ycQ6khVQt4IgHX2jNI=
2378+
23742379
immutable@^3.7.4:
23752380
version "3.8.2"
23762381
resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3"

0 commit comments

Comments
 (0)