Skip to content

Commit 5dd100a

Browse files
authored
Merge pull request #23 from dai-shi/slimmed-deep-proxy
Slimmed deep proxy
2 parents a2346b1 + 4890262 commit 5dd100a

File tree

16 files changed

+1518
-848
lines changed

16 files changed

+1518
-848
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Change Log
22

33
## [Unreleased]
4+
### Changed
5+
- New deep proxy instead or proxyequal
46

57
## [2.0.1] - 2019-04-15
68
### Changed

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,31 @@ You can also try them in codesandbox.io:
154154
[09](https://codesandbox.io/s/github/dai-shi/reactive-react-redux/tree/master/examples/09_thunk)
155155
[10](https://codesandbox.io/s/github/dai-shi/reactive-react-redux/tree/master/examples/10_selectors)
156156

157+
## Limitations
158+
159+
By relying on Proxy,
160+
there are some false negatives (failure to trigger re-renders)
161+
and some false positives (extra re-renders) in edge cases.
162+
163+
### Proxied states are referentially equal only in per-hook basis
164+
165+
```javascript
166+
const state1 = useReduxState();
167+
const state2 = useReduxState();
168+
// state1 and state2 is referentially not equal
169+
// even if the underlying redux state is referentially equal.
170+
```
171+
172+
### An object referential change doesn't trigger re-render if at least one object property is accessed in previous render
173+
174+
```javascript
175+
const state = useReduxState();
176+
const foo = useMemo(() => state.foo, [state]);
177+
const bar = state.bar;
178+
// if state.foo is not evaluated in render,
179+
// it won't trigger re-render if only state.foo is changed.
180+
```
181+
157182
## Blogs
158183

159184
- [A deadly simple React bindings library for Redux with Hooks API](https://medium.com/@dai_shi/a-deadly-simple-react-bindings-library-for-redux-with-hooks-api-822295857282)

__tests__/01_basic_spec.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ describe('basic spec', () => {
4646
</ReduxProvider>
4747
</StrictMode>
4848
);
49-
const { getByText, container } = render(<App />);
49+
const { getAllByText, container } = render(<App />);
5050
expect(container).toMatchSnapshot();
51-
fireEvent.click(getByText('+1'));
51+
fireEvent.click(getAllByText('+1')[0]);
5252
expect(container).toMatchSnapshot();
5353
});
5454
});

__tests__/02_selectors_spec.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,14 +113,14 @@ describe('selectors spec', () => {
113113
</ReduxProvider>
114114
</StrictMode>
115115
);
116-
const { getByText, container } = render(<App />);
116+
const { getAllByText, container } = render(<App />);
117117
expect(numOfRenders1).toBe(4); // doubled because of StrictMode
118118
expect(container).toMatchSnapshot();
119-
fireEvent.click(getByText('inc1'));
119+
fireEvent.click(getAllByText('inc1')[0]);
120120
expect(numOfRenders1).toBe(4);
121-
fireEvent.click(getByText('inc1'));
121+
fireEvent.click(getAllByText('inc1')[0]);
122122
expect(numOfRenders1).toBe(4);
123-
fireEvent.click(getByText('inc1'));
123+
fireEvent.click(getAllByText('inc1')[0]);
124124
expect(numOfRenders1).toBe(8);
125125
expect(container).toMatchSnapshot();
126126
});

__tests__/10_deep_proxy_spec.js

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
import { createDeepProxy, isDeepChanged } from '../src/utils';
2+
3+
const noop = () => {};
4+
5+
describe('shallow object spec', () => {
6+
it('no property access', () => {
7+
const s1 = { a: 'a', b: 'b' };
8+
const a1 = new WeakMap();
9+
const p1 = createDeepProxy(s1, a1);
10+
noop(p1);
11+
expect(isDeepChanged(s1, { a: 'a', b: 'b' }, a1)).toBe(false);
12+
expect(isDeepChanged(s1, { a: 'a2', b: 'b' }, a1)).toBe(false);
13+
expect(isDeepChanged(s1, { a: 'a', b: 'b2' }, a1)).toBe(false);
14+
});
15+
16+
it('one property access', () => {
17+
const s1 = { a: 'a', b: 'b' };
18+
const a1 = new WeakMap();
19+
const p1 = createDeepProxy(s1, a1);
20+
noop(p1.a);
21+
expect(isDeepChanged(s1, { a: 'a', b: 'b' }, a1)).toBe(false);
22+
expect(isDeepChanged(s1, { a: 'a2', b: 'b' }, a1)).toBe(true);
23+
expect(isDeepChanged(s1, { a: 'a', b: 'b2' }, a1)).toBe(false);
24+
});
25+
});
26+
27+
describe('deep object spec', () => {
28+
it('intermediate property access', () => {
29+
const s1 = { a: { b: 'b', c: 'c' } };
30+
const a1 = new WeakMap();
31+
const p1 = createDeepProxy(s1, a1);
32+
noop(p1.a);
33+
expect(isDeepChanged(s1, { a: s1.a }, a1)).toBe(false);
34+
expect(isDeepChanged(s1, { a: { b: 'b2', c: 'c' } }, a1)).toBe(true);
35+
expect(isDeepChanged(s1, { a: { b: 'b', c: 'c2' } }, a1)).toBe(true);
36+
});
37+
38+
it('leaf property access', () => {
39+
const s1 = { a: { b: 'b', c: 'c' } };
40+
const a1 = new WeakMap();
41+
const p1 = createDeepProxy(s1, a1);
42+
noop(p1.a.b);
43+
expect(isDeepChanged(s1, { a: s1.a }, a1)).toBe(false);
44+
expect(isDeepChanged(s1, { a: { b: 'b2', c: 'c' } }, a1)).toBe(true);
45+
expect(isDeepChanged(s1, { a: { b: 'b', c: 'c2' } }, a1)).toBe(false);
46+
});
47+
});
48+
49+
describe('reference equality spec', () => {
50+
it('simple', () => {
51+
const proxyCache = new WeakMap();
52+
const s1 = { a: 'a', b: 'b' };
53+
const a1 = new WeakMap();
54+
const p1 = createDeepProxy(s1, a1, proxyCache);
55+
noop(p1.a);
56+
const s2 = s1; // keep the reference
57+
const a2 = new WeakMap();
58+
const p2 = createDeepProxy(s2, a2, proxyCache);
59+
noop(p2.b);
60+
expect(p1).toBe(p2);
61+
expect(isDeepChanged(s1, { a: 'a', b: 'b' }, a1)).toBe(false);
62+
expect(isDeepChanged(s1, { a: 'a2', b: 'b' }, a1)).toBe(true);
63+
expect(isDeepChanged(s1, { a: 'a', b: 'b2' }, a1)).toBe(false);
64+
expect(isDeepChanged(s2, { a: 'a', b: 'b' }, a2)).toBe(false);
65+
expect(isDeepChanged(s2, { a: 'a2', b: 'b' }, a2)).toBe(false);
66+
expect(isDeepChanged(s2, { a: 'a', b: 'b2' }, a2)).toBe(true);
67+
});
68+
69+
it('nested', () => {
70+
const proxyCache = new WeakMap();
71+
const s1 = { a: { b: 'b', c: 'c' } };
72+
const a1 = new WeakMap();
73+
const p1 = createDeepProxy(s1, a1, proxyCache);
74+
noop(p1.a.b);
75+
const s2 = { a: s1.a }; // keep the reference
76+
const a2 = new WeakMap();
77+
const p2 = createDeepProxy(s2, a2, proxyCache);
78+
noop(p2.a.c);
79+
expect(p1).not.toBe(p2);
80+
expect(p1.a).toBe(p2.a);
81+
expect(isDeepChanged(s1, { a: { b: 'b', c: 'c' } }, a1)).toBe(false);
82+
expect(isDeepChanged(s1, { a: { b: 'b2', c: 'c' } }, a1)).toBe(true);
83+
expect(isDeepChanged(s1, { a: { b: 'b', c: 'c2' } }, a1)).toBe(false);
84+
expect(isDeepChanged(s2, { a: { b: 'b', c: 'c' } }, a2)).toBe(false);
85+
expect(isDeepChanged(s2, { a: { b: 'b2', c: 'c' } }, a2)).toBe(false);
86+
expect(isDeepChanged(s2, { a: { b: 'b', c: 'c2' } }, a2)).toBe(true);
87+
});
88+
});
89+
90+
describe('array spec', () => {
91+
it('length', () => {
92+
const s1 = [1, 2, 3];
93+
const a1 = new WeakMap();
94+
const p1 = createDeepProxy(s1, a1);
95+
noop(p1.length);
96+
expect(isDeepChanged(s1, [1, 2, 3], a1)).toBe(false);
97+
expect(isDeepChanged(s1, [1, 2, 3, 4], a1)).toBe(true);
98+
expect(isDeepChanged(s1, [1, 2], a1)).toBe(true);
99+
expect(isDeepChanged(s1, [1, 2, 4], a1)).toBe(false);
100+
});
101+
102+
it('forEach', () => {
103+
const s1 = [1, 2, 3];
104+
const a1 = new WeakMap();
105+
const p1 = createDeepProxy(s1, a1);
106+
p1.forEach(noop);
107+
expect(isDeepChanged(s1, [1, 2, 3], a1)).toBe(false);
108+
expect(isDeepChanged(s1, [1, 2, 3, 4], a1)).toBe(true);
109+
expect(isDeepChanged(s1, [1, 2], a1)).toBe(true);
110+
expect(isDeepChanged(s1, [1, 2, 4], a1)).toBe(true);
111+
});
112+
113+
it('for-of', () => {
114+
const s1 = [1, 2, 3];
115+
const a1 = new WeakMap();
116+
const p1 = createDeepProxy(s1, a1);
117+
// eslint-disable-next-line no-restricted-syntax
118+
for (const x of p1) {
119+
noop(x);
120+
}
121+
expect(isDeepChanged(s1, [1, 2, 3], a1)).toBe(false);
122+
expect(isDeepChanged(s1, [1, 2, 3, 4], a1)).toBe(true);
123+
expect(isDeepChanged(s1, [1, 2], a1)).toBe(true);
124+
expect(isDeepChanged(s1, [1, 2, 4], a1)).toBe(true);
125+
});
126+
});
127+
128+
describe('keys spec', () => {
129+
it('object keys', () => {
130+
const s1 = { a: { b: 'b' }, c: 'c' };
131+
const a1 = new WeakMap();
132+
const p1 = createDeepProxy(s1, a1);
133+
noop(Object.keys(p1));
134+
expect(isDeepChanged(s1, { a: s1.a, c: 'c' }, a1)).toBe(false);
135+
expect(isDeepChanged(s1, { a: { b: 'b' }, c: 'c' }, a1)).toBe(false);
136+
expect(isDeepChanged(s1, { a: s1.a }, a1)).toBe(true);
137+
expect(isDeepChanged(s1, { a: s1.a, c: 'c', d: 'd' }, a1)).toBe(true);
138+
});
139+
140+
it('for-in', () => {
141+
const s1 = { a: { b: 'b' }, c: 'c' };
142+
const a1 = new WeakMap();
143+
const p1 = createDeepProxy(s1, a1);
144+
// eslint-disable-next-line no-restricted-syntax, guard-for-in
145+
for (const k in p1) {
146+
noop(k);
147+
}
148+
expect(isDeepChanged(s1, { a: s1.a, c: 'c' }, a1)).toBe(false);
149+
expect(isDeepChanged(s1, { a: { b: 'b' }, c: 'c' }, a1)).toBe(false);
150+
expect(isDeepChanged(s1, { a: s1.a }, a1)).toBe(true);
151+
expect(isDeepChanged(s1, { a: s1.a, c: 'c', d: 'd' }, a1)).toBe(true);
152+
});
153+
154+
it('single in operator', () => {
155+
const s1 = { a: { b: 'b' }, c: 'c' };
156+
const a1 = new WeakMap();
157+
const p1 = createDeepProxy(s1, a1);
158+
noop('a' in p1);
159+
expect(isDeepChanged(s1, { a: s1.a, c: 'c' }, a1)).toBe(false);
160+
expect(isDeepChanged(s1, { a: s1.a }, a1)).toBe(false);
161+
expect(isDeepChanged(s1, { c: 'c', d: 'd' }, a1)).toBe(true);
162+
});
163+
});
164+
165+
166+
describe('special objects spec', () => {
167+
it('object with cycles', () => {
168+
const proxyCache = new WeakMap();
169+
const s1 = { a: 'a' };
170+
s1.self = s1;
171+
const a1 = new WeakMap();
172+
const p1 = createDeepProxy(s1, a1, proxyCache);
173+
const c1 = new WeakMap();
174+
noop(p1.self.a);
175+
expect(isDeepChanged(s1, s1, a1, c1)).toBe(false);
176+
expect(isDeepChanged(s1, { a: 'a', self: s1 }, a1, c1)).toBe(false);
177+
const s2 = { a: 'a' };
178+
s2.self = s2;
179+
expect(isDeepChanged(s1, s2, a1, c1)).toBe(false);
180+
const s3 = { a: 'a2' };
181+
s3.self = s3;
182+
expect(isDeepChanged(s1, s3, a1, c1)).toBe(true);
183+
});
184+
185+
it('object with cycles 2', () => {
186+
const proxyCache = new WeakMap();
187+
const s1 = { a: { b: 'b' } };
188+
s1.self = s1;
189+
const a1 = new WeakMap();
190+
const p1 = createDeepProxy(s1, a1, proxyCache);
191+
const c1 = new WeakMap();
192+
noop(p1.self.a);
193+
expect(isDeepChanged(s1, s1, a1, c1)).toBe(false);
194+
expect(isDeepChanged(s1, { a: s1.a, self: s1 }, a1, c1)).toBe(false);
195+
const s2 = { a: { b: 'b' } };
196+
s2.self = s2;
197+
expect(isDeepChanged(s1, s2, a1, c1)).toBe(true);
198+
});
199+
200+
it('frozen object', () => {
201+
const proxyCache = new WeakMap();
202+
const s1 = { a: { b: 'b' } };
203+
Object.freeze(s1);
204+
const a1 = new WeakMap();
205+
const p1 = createDeepProxy(s1, a1, proxyCache);
206+
noop(p1.a.b);
207+
expect(isDeepChanged(s1, s1, a1)).toBe(false);
208+
expect(isDeepChanged(s1, { a: { b: 'b' } }, a1)).toBe(false);
209+
expect(isDeepChanged(s1, { a: { b: 'b2' } }, a1)).toBe(true);
210+
});
211+
});
212+
213+
describe('builtin objects spec', () => {
214+
// we can't track builtin objects
215+
216+
it('boolean', () => {
217+
/* eslint-disable no-new-wrappers */
218+
const proxyCache = new WeakMap();
219+
const s1 = { a: new Boolean(false) };
220+
const a1 = new WeakMap();
221+
const p1 = createDeepProxy(s1, a1, proxyCache);
222+
noop(p1.a.valueOf());
223+
expect(isDeepChanged(s1, s1, a1)).toBe(false);
224+
expect(isDeepChanged(s1, { a: new Boolean(false) }, a1)).toBe(true);
225+
/* eslint-enable no-new-wrappers */
226+
});
227+
228+
it('error', () => {
229+
const proxyCache = new WeakMap();
230+
const s1 = { a: new Error('e') };
231+
const a1 = new WeakMap();
232+
const p1 = createDeepProxy(s1, a1, proxyCache);
233+
noop(p1.a.message);
234+
expect(isDeepChanged(s1, s1, a1)).toBe(false);
235+
expect(isDeepChanged(s1, { a: new Error('e') }, a1)).toBe(true);
236+
});
237+
238+
it('date', () => {
239+
const proxyCache = new WeakMap();
240+
const s1 = { a: new Date('2019-05-11T12:22:29.293Z') };
241+
const a1 = new WeakMap();
242+
const p1 = createDeepProxy(s1, a1, proxyCache);
243+
noop(p1.a.getTime());
244+
expect(isDeepChanged(s1, s1, a1)).toBe(false);
245+
expect(isDeepChanged(s1, { a: new Date('2019-05-11T12:22:29.293Z') }, a1)).toBe(true);
246+
});
247+
248+
it('regexp', () => {
249+
const proxyCache = new WeakMap();
250+
const s1 = { a: /a/ };
251+
const a1 = new WeakMap();
252+
const p1 = createDeepProxy(s1, a1, proxyCache);
253+
noop(p1.a.test('a'));
254+
expect(isDeepChanged(s1, s1, a1)).toBe(false);
255+
expect(isDeepChanged(s1, { a: /a/ }, a1)).toBe(true);
256+
});
257+
258+
it('map', () => {
259+
const proxyCache = new WeakMap();
260+
const s1 = { a: new Map() };
261+
const a1 = new WeakMap();
262+
const p1 = createDeepProxy(s1, a1, proxyCache);
263+
noop(p1.a.entries());
264+
expect(isDeepChanged(s1, s1, a1)).toBe(false);
265+
expect(isDeepChanged(s1, { a: new Map() }, a1)).toBe(true);
266+
});
267+
268+
it('typed array', () => {
269+
const proxyCache = new WeakMap();
270+
const s1 = { a: Int8Array.from([1]) };
271+
const a1 = new WeakMap();
272+
const p1 = createDeepProxy(s1, a1, proxyCache);
273+
noop(p1.a[0]);
274+
expect(isDeepChanged(s1, s1, a1)).toBe(false);
275+
expect(isDeepChanged(s1, { a: Int8Array.from([1]) }, a1)).toBe(true);
276+
});
277+
});

dist/index.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ Object.defineProperty(exports, "useReduxState", {
2121
return _useReduxState.useReduxState;
2222
}
2323
});
24+
Object.defineProperty(exports, "useReduxStateRich", {
25+
enumerable: true,
26+
get: function get() {
27+
return _useReduxState.useReduxStateRich;
28+
}
29+
});
2430
Object.defineProperty(exports, "useReduxStateSimple", {
2531
enumerable: true,
2632
get: function get() {

0 commit comments

Comments
 (0)