Skip to content
This repository was archived by the owner on May 20, 2022. It is now read-only.

Commit 7419122

Browse files
committed
adding memoization tests, improving code readbility
1 parent 2b532da commit 7419122

File tree

2 files changed

+310
-24
lines changed

2 files changed

+310
-24
lines changed

src/index.js

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ let stores = {};
44
let subscriptions = {};
55

66
const defaultReducer = (state, payload) => payload;
7-
const defaultMapDependencies = (state) => state;
7+
const defaultMemoFn = (state) => state;
88

99
/** The public interface of a store */
1010
class StoreInterface {
@@ -78,8 +78,12 @@ export function createStore(name, state = {}, reducer=defaultReducer) {
7878
state,
7979
reducer,
8080
setState(action, callback) {
81-
if (this.reducer === defaultReducer && action === this.state && typeof action !== 'object') {
82-
console.log('basic memoization, not updating');
81+
const isPrimitiveStateWithoutReducerAndIsPreviousState =
82+
this.reducer === defaultReducer
83+
&& action === this.state
84+
&& typeof action !== 'object';
85+
86+
if (isPrimitiveStateWithoutReducerAndIsPreviousState) {
8387
if (typeof callback === 'function') callback(this.state)
8488
return;
8589
}
@@ -88,16 +92,14 @@ export function createStore(name, state = {}, reducer=defaultReducer) {
8892
const newState = this.reducer(this.state, action);
8993
this.state = newState;
9094

91-
this.settersPerDependency.forEach((setters, mapDependency) => {
92-
const prevResult = mapDependency(currentState);
93-
const newResult = mapDependency(newState);
95+
this.updatersPerMemoFunction.forEach((updaters, memoFn) => {
96+
const prevResult = memoFn(currentState);
97+
const newResult = memoFn(newState);
9498
if (prevResult === newResult) {
95-
console.log('advanced memoization, not updating');
9699
return;
97100
}
98-
for (let setter of setters) {
99-
setter(this.state);
100-
console.log('updating');
101+
for (let updateComponent of updaters) {
102+
updateComponent(this.state);
101103
}
102104
});
103105

@@ -107,13 +109,15 @@ export function createStore(name, state = {}, reducer=defaultReducer) {
107109

108110
if (typeof callback === 'function') callback(this.state)
109111
},
110-
settersPerDependency: new Map(),
112+
updatersPerMemoFunction: new Map(),
111113
};
114+
112115
store.setState = store.setState.bind(store);
116+
store.updatersPerMemoFunction.set(defaultMemoFn, new Set())
117+
stores = Object.assign({}, stores, { [name]: store });
113118
subscriptions[name] = [];
114-
store.settersPerDependency.set(defaultMapDependencies, new Set())
119+
115120
store.public = new StoreInterface(name, store, reducer !== defaultReducer);
116-
stores = Object.assign({}, stores, { [name]: store });
117121
return store.public;
118122
}
119123

@@ -134,34 +138,40 @@ export function getStoreByName(name) {
134138
/**
135139
* Returns a [ state, setState ] pair for the selected store. Can only be called within React Components
136140
* @param {String|StoreInterface} identifier - The identifier for the wanted store
141+
* @callback memoFn [state => state] - A memoization function to optimize component rerender. Receive the store state and return a subset of it. The component will only rerender when the subset changes.
137142
* @returns {Array} the [state, setState] pair.
138143
*/
139-
export function useStore(identifier, mapDependency=defaultMapDependencies) {
144+
145+
/**
146+
*
147+
* @param {memoFn} state
148+
*/
149+
export function useStore(identifier, memoFn=defaultMemoFn) {
140150
const store = getStoreByIdentifier(identifier);
141151
if (!store) {
142152
throw 'store does not exist';
143153
}
144-
if (typeof mapDependency !== 'function') {
145-
throw 'dependencyMap must be a function';
154+
if (typeof memoFn !== 'function') {
155+
throw 'memoFn must be a function';
146156
}
147157

148158
const [ state, set ] = useState(store.state);
149159

150160
useEffect(() => {
151-
if (!store.settersPerDependency.has(mapDependency)) {
152-
store.settersPerDependency.set(mapDependency, new Set());
161+
if (!store.updatersPerMemoFunction.has(memoFn)) {
162+
store.updatersPerMemoFunction.set(memoFn, new Set());
153163
}
154164

155-
const settersPerDependency = store.settersPerDependency.get(mapDependency);
165+
const updatersPerMemoFunction = store.updatersPerMemoFunction.get(memoFn);
156166

157-
if (!settersPerDependency.has(set)) {
158-
settersPerDependency.add(set);
167+
if (!updatersPerMemoFunction.has(set)) {
168+
updatersPerMemoFunction.add(set);
159169
}
160170

161171
return () => {
162-
settersPerDependency.delete(set);
163-
if (!settersPerDependency.size) {
164-
store.settersPerDependency.delete(mapDependency);
172+
updatersPerMemoFunction.delete(set);
173+
if (!updatersPerMemoFunction.size) {
174+
store.updatersPerMemoFunction.delete(memoFn);
165175
}
166176
}
167177
}, [])

src/tests/memoization.test.js

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
import React, { useEffect } from 'react';
2+
import { mount } from 'enzyme';
3+
import { act } from 'react-dom/test-utils';
4+
import { createStore, useStore } from '..';
5+
6+
describe('store memoization', () => {
7+
it('Should not update components if setState is called with the same previous state', (done) => {
8+
const store = createStore('store', 0);
9+
const componentRenderCount = jest.fn();
10+
const anotherComponentRenderCount = jest.fn();
11+
12+
const Component = (props) => {
13+
const [state, setState] = useStore(store);
14+
15+
componentRenderCount();
16+
17+
return (
18+
<button onClick={() => setState(state+1)}>
19+
This button has been clicked {state} times
20+
</button>
21+
);
22+
}
23+
24+
const AnotherComponent = (props) => {
25+
const [state] = useStore(store);
26+
27+
anotherComponentRenderCount();
28+
29+
return (
30+
<div>
31+
{state}
32+
</div>
33+
);
34+
}
35+
36+
const renderedComponent = mount(<Component />);
37+
const renderedAnotherComponent = mount(<AnotherComponent />);
38+
39+
requestAnimationFrame(() => {
40+
expect(componentRenderCount).toHaveBeenCalledTimes(1);
41+
expect(anotherComponentRenderCount).toHaveBeenCalledTimes(1);
42+
43+
renderedComponent.find('button').simulate('click');
44+
expect(componentRenderCount).toHaveBeenCalledTimes(2);
45+
expect(anotherComponentRenderCount).toHaveBeenCalledTimes(2);
46+
47+
act(() => store.setState(3));
48+
expect(componentRenderCount).toHaveBeenCalledTimes(3);
49+
expect(anotherComponentRenderCount).toHaveBeenCalledTimes(3);
50+
51+
act(() => store.setState(3));
52+
expect(componentRenderCount).toHaveBeenCalledTimes(3);
53+
expect(anotherComponentRenderCount).toHaveBeenCalledTimes(3);
54+
55+
act(() => store.setState(3));
56+
expect(componentRenderCount).toHaveBeenCalledTimes(3);
57+
expect(anotherComponentRenderCount).toHaveBeenCalledTimes(3);
58+
59+
act(() => store.setState(1));
60+
expect(componentRenderCount).toHaveBeenCalledTimes(4);
61+
expect(anotherComponentRenderCount).toHaveBeenCalledTimes(4);
62+
63+
act(() => store.setState(1));
64+
expect(componentRenderCount).toHaveBeenCalledTimes(4);
65+
expect(anotherComponentRenderCount).toHaveBeenCalledTimes(4);
66+
67+
expect(renderedComponent.text()).toBe('This button has been clicked 1 times')
68+
expect(renderedAnotherComponent.text()).toBe('1')
69+
70+
done();
71+
})
72+
});
73+
74+
it('Should always update components if state is complex and no mapDeps is provided', (done) => {
75+
const store = createStore('complexState', { number: 0 });
76+
const componentRenderCount = jest.fn();
77+
const anotherComponentRenderCount = jest.fn();
78+
79+
const Component = (props) => {
80+
const [state, setState] = useStore(store);
81+
82+
componentRenderCount();
83+
84+
return (
85+
<button onClick={() => setState({ number: state.number + 1})}>
86+
This button has been clicked {state.number} times
87+
</button>
88+
);
89+
}
90+
91+
const AnotherComponent = (props) => {
92+
const [state] = useStore(store);
93+
94+
anotherComponentRenderCount();
95+
96+
return (
97+
<div>
98+
{state.number}
99+
</div>
100+
);
101+
}
102+
103+
const renderedComponent = mount(<Component />);
104+
const renderedAnotherComponent = mount(<AnotherComponent />);
105+
106+
requestAnimationFrame(() => {
107+
expect(componentRenderCount).toHaveBeenCalledTimes(1);
108+
expect(anotherComponentRenderCount).toHaveBeenCalledTimes(1);
109+
110+
renderedComponent.find('button').simulate('click');
111+
expect(componentRenderCount).toHaveBeenCalledTimes(2);
112+
expect(anotherComponentRenderCount).toHaveBeenCalledTimes(2);
113+
114+
act(() => store.setState({ number: 3 }));
115+
expect(componentRenderCount).toHaveBeenCalledTimes(3);
116+
expect(anotherComponentRenderCount).toHaveBeenCalledTimes(3);
117+
118+
act(() => store.setState({ number: 3 }));
119+
expect(componentRenderCount).toHaveBeenCalledTimes(4);
120+
expect(anotherComponentRenderCount).toHaveBeenCalledTimes(4);
121+
122+
act(() => store.setState({ number: 3 }));
123+
expect(componentRenderCount).toHaveBeenCalledTimes(5);
124+
expect(anotherComponentRenderCount).toHaveBeenCalledTimes(5);
125+
126+
act(() => store.setState({ number: 1 }));
127+
expect(componentRenderCount).toHaveBeenCalledTimes(6);
128+
expect(anotherComponentRenderCount).toHaveBeenCalledTimes(6);
129+
130+
act(() => store.setState({ number: 1 }));
131+
expect(componentRenderCount).toHaveBeenCalledTimes(7);
132+
expect(anotherComponentRenderCount).toHaveBeenCalledTimes(7);
133+
134+
expect(renderedComponent.text()).toBe('This button has been clicked 1 times')
135+
expect(renderedAnotherComponent.text()).toBe('1')
136+
137+
done();
138+
})
139+
});
140+
141+
it('Should only update component if the field it wants is updated', (done) => {
142+
const store = createStore('complexStore2', {foo: { bar: 2 }, baz: 3});
143+
144+
const componentRenderCount = jest.fn();
145+
const anotherComponentRenderCount = jest.fn();
146+
147+
const memoBar = (state) => state.foo && state.foo.bar;
148+
const memoBaz = (state) => state.baz;
149+
150+
const Component = (props) => {
151+
const [state] = useStore(store, memoBar);
152+
153+
componentRenderCount();
154+
155+
return (
156+
<div>
157+
{state.foo && state.foo.bar}
158+
</div>
159+
);
160+
}
161+
162+
const AnotherComponent = (props) => {
163+
const [state] = useStore(store, memoBaz);
164+
165+
anotherComponentRenderCount();
166+
167+
return (
168+
<div>
169+
{state.baz}
170+
</div>
171+
);
172+
}
173+
174+
const renderedComponent = mount(<Component />);
175+
const renderedAnotherComponent = mount(<AnotherComponent />);
176+
177+
requestAnimationFrame(() => {
178+
expect(componentRenderCount).toHaveBeenCalledTimes(1);
179+
expect(anotherComponentRenderCount).toHaveBeenCalledTimes(1);
180+
181+
act(() => store.setState({...store.getState(), baz: 2}));
182+
expect(componentRenderCount).toHaveBeenCalledTimes(1);
183+
expect(anotherComponentRenderCount).toHaveBeenCalledTimes(2);
184+
185+
act(() => store.setState({...store.getState(), foo: {bar: 2}}));
186+
expect(componentRenderCount).toHaveBeenCalledTimes(1);
187+
expect(anotherComponentRenderCount).toHaveBeenCalledTimes(2);
188+
189+
act(() => store.setState({...store.getState(), foo: {bar: 3}}));
190+
expect(componentRenderCount).toHaveBeenCalledTimes(2);
191+
192+
act(() => store.setState({...store.getState()}));
193+
expect(componentRenderCount).toHaveBeenCalledTimes(2);
194+
expect(anotherComponentRenderCount).toHaveBeenCalledTimes(2);
195+
196+
act(() => store.setState({...store.getState(), foo: {}}));
197+
expect(componentRenderCount).toHaveBeenCalledTimes(3);
198+
expect(anotherComponentRenderCount).toHaveBeenCalledTimes(2);
199+
200+
act(() => store.setState({ foo: { bar: 3 }, baz: 10 }));
201+
expect(componentRenderCount).toHaveBeenCalledTimes(4);
202+
expect(anotherComponentRenderCount).toHaveBeenCalledTimes(3);
203+
204+
done();
205+
})
206+
});
207+
});
208+
209+
describe('store memoization with dispatch', () => {
210+
it('Should not update components if setState is called with the same previous state', (done) => {
211+
const store = createStore('reduceredStore', 0, (state, payload) => payload);
212+
const componentRenderCount = jest.fn();
213+
const anotherComponentRenderCount = jest.fn();
214+
215+
const Component = (props) => {
216+
const [state, dispatch] = useStore(store);
217+
218+
componentRenderCount();
219+
220+
return (
221+
<button onClick={() => dispatch(state + 1)}>
222+
This button has been clicked {state} times
223+
</button>
224+
);
225+
}
226+
227+
const AnotherComponent = (props) => {
228+
const [state] = useStore(store, );
229+
230+
anotherComponentRenderCount();
231+
232+
return (
233+
<div>
234+
{state}
235+
</div>
236+
);
237+
}
238+
239+
const renderedComponent = mount(<Component />);
240+
const renderedAnotherComponent = mount(<AnotherComponent />);
241+
242+
requestAnimationFrame(() => {
243+
expect(componentRenderCount).toHaveBeenCalledTimes(1);
244+
expect(anotherComponentRenderCount).toHaveBeenCalledTimes(1);
245+
246+
renderedComponent.find('button').simulate('click');
247+
expect(componentRenderCount).toHaveBeenCalledTimes(2);
248+
expect(anotherComponentRenderCount).toHaveBeenCalledTimes(2);
249+
250+
act(() => store.dispatch(3));
251+
expect(componentRenderCount).toHaveBeenCalledTimes(3);
252+
expect(anotherComponentRenderCount).toHaveBeenCalledTimes(3);
253+
254+
act(() => store.dispatch(3));
255+
expect(componentRenderCount).toHaveBeenCalledTimes(3);
256+
expect(anotherComponentRenderCount).toHaveBeenCalledTimes(3);
257+
258+
act(() => store.dispatch(3));
259+
expect(componentRenderCount).toHaveBeenCalledTimes(3);
260+
expect(anotherComponentRenderCount).toHaveBeenCalledTimes(3);
261+
262+
act(() => store.dispatch(1));
263+
expect(componentRenderCount).toHaveBeenCalledTimes(4);
264+
expect(anotherComponentRenderCount).toHaveBeenCalledTimes(4);
265+
266+
act(() => store.dispatch(1));
267+
expect(componentRenderCount).toHaveBeenCalledTimes(4);
268+
expect(anotherComponentRenderCount).toHaveBeenCalledTimes(4);
269+
270+
expect(renderedComponent.text()).toBe('This button has been clicked 1 times')
271+
expect(renderedAnotherComponent.text()).toBe('1')
272+
273+
done();
274+
})
275+
});
276+
});

0 commit comments

Comments
 (0)