Skip to content

Commit 7cc2155

Browse files
committed
2 times faster
1 parent 814c690 commit 7cc2155

File tree

10 files changed

+4969
-171
lines changed

10 files changed

+4969
-171
lines changed

.babelrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
"presets": ["env"]
2+
"presets": ["env", "react"]
33
}

README.md

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {connect, Provider} from 'beautiful-react-redux';
2626

2727
100% compatible with any other memoization you might already had underneath.
2828

29-
#### Doubke check your existing selectors
29+
#### Double check your existing selectors
3030
If you already handling selectors by your own, and dont need external tools -
3131
you can just double check that your mapStateToProps is good enough.
3232
```js
@@ -36,14 +36,6 @@ import 'beautiful-react-redux/check';
3636

3737
PS: Better not to mix memoize and check.
3838

39-
# IE11 and React-native users!
40-
This library uses ES6 Proxy and Reflection underneath. In order to run it you
41-
have to include polyfill - https://github.com/tvcutsem/harmony-reflect
42-
43-
```js
44-
npm install harmony-reflect
45-
```
46-
4739
Consider double measure performance, or use only `beautiful-react-redux/check` and another memoization library.
4840

4941
# Licence

_tests/index.spec.js

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import React from 'react';
2+
import {ReduxFocus} from 'react-redux-focus';
3+
import {connect, connectAndCheck} from '../src';
4+
5+
import EnzymeReactAdapter from 'enzyme-adapter-react-16';
6+
import {mount, configure as configureEnzyme} from 'enzyme';
7+
8+
configureEnzyme({adapter: new EnzymeReactAdapter()});
9+
10+
11+
describe('Test', () => {
12+
it('should secure memoization', () => {
13+
const cb = jest.fn();
14+
const rb = jest.fn();
15+
const mapStateToProps = state => {
16+
cb();
17+
return {number: state.key}
18+
};
19+
20+
const Component = ({number}) => {
21+
rb();
22+
return <div>{number}</div>
23+
};
24+
const ConnectedComponent = connect(mapStateToProps)(Component);
25+
26+
const wrapper = mount(
27+
<ReduxFocus focus={(state, props) => props.state} state={{key: 'key1-value', key2: 2}}>
28+
<ConnectedComponent/>
29+
</ReduxFocus>
30+
);
31+
expect(wrapper.html()).toMatch(/key1-value/);
32+
expect(cb).toHaveBeenCalledTimes(1);
33+
expect(rb).toHaveBeenCalledTimes(1);
34+
35+
wrapper.setProps({state: {key: 'key1-value', key2: 3}});
36+
37+
expect(wrapper.html()).toMatch(/key1-value/);
38+
expect(cb).toHaveBeenCalledTimes(1);
39+
expect(rb).toHaveBeenCalledTimes(1);
40+
41+
wrapper.setProps({state: {key: 'key2-value', key2: 3}});
42+
43+
expect(wrapper.html()).toMatch(/key2-value/);
44+
expect(cb).toHaveBeenCalledTimes(2);
45+
expect(rb).toHaveBeenCalledTimes(2);
46+
47+
expect(ConnectedComponent.__trackingPaths).toEqual(['key']);
48+
});
49+
50+
it('should maintain per-instance memoization', () => {
51+
const cb = jest.fn();
52+
const mapStateToProps = state => {
53+
cb();
54+
return {number: state.key}
55+
};
56+
57+
const Component = ({number}) => <div>{number}</div>;
58+
const ConnectedComponent = connect(mapStateToProps)(Component);
59+
60+
const wrapper = mount(
61+
<ReduxFocus focus={(state, props) => props.state} state={{key1: 'key1-value', key2: 'key2-value'}}>
62+
<ReduxFocus focus={(state) => ({key: state.key1})}>
63+
<ConnectedComponent/>
64+
</ReduxFocus>
65+
<ReduxFocus focus={(state) => ({key: state.key2})}>
66+
<ConnectedComponent/>
67+
</ReduxFocus>
68+
</ReduxFocus>
69+
);
70+
expect(cb).toHaveBeenCalledTimes(2);
71+
72+
wrapper.setProps({state: {key1: 'key1-value', key2: 'key2-value', key3: 'key3'}});
73+
expect(cb).toHaveBeenCalledTimes(2);
74+
75+
wrapper.setProps({state: {key1: 'key1-1value', key2: 'key2-value'}});
76+
expect(cb).toHaveBeenCalledTimes(3);
77+
78+
wrapper.setProps({state: {key1: 'key1-1value', key2: 'key2-1value'}});
79+
expect(cb).toHaveBeenCalledTimes(4);
80+
81+
wrapper.setProps({state: {key1: 'key1-value', key2: 'key2-value'}});
82+
expect(cb).toHaveBeenCalledTimes(6);
83+
84+
expect(ConnectedComponent.__trackingPaths).toEqual(['key']);
85+
});
86+
87+
it('should maintain per-instance memoization', () => {
88+
const cb = jest.fn();
89+
const mapStateToProps = (state, props) => {
90+
cb();
91+
return {number: state[props.keyName], test: state.flag ? state.secret : 0}
92+
};
93+
94+
const Component = ({number}) => <div>{number}</div>;
95+
const ConnectedComponent = connect(mapStateToProps)(Component);
96+
97+
const wrapper = mount(
98+
<ReduxFocus focus={(state, props) => props.state} state={{key1: 'key1-value', key2: 'key2-value'}}>
99+
<ConnectedComponent keyName="key1"/>
100+
<ConnectedComponent keyName="key2"/>
101+
</ReduxFocus>
102+
);
103+
expect(cb).toHaveBeenCalledTimes(2);
104+
105+
wrapper.setProps({state: {key1: 'key1-value', key2: 'key2-value', key3: 'key3'}});
106+
expect(cb).toHaveBeenCalledTimes(2);
107+
108+
expect(ConnectedComponent.__trackingPaths).toEqual(['key1', 'flag', 'key2']);
109+
110+
wrapper.setProps({state: {key1: 'key1-1value', key2: 'key2-value', flag: true}});
111+
expect(cb).toHaveBeenCalledTimes(4);
112+
expect(ConnectedComponent.__trackingPaths).toEqual(['key1', 'flag', 'key2', 'secret']);
113+
});
114+
115+
it('should check the purity of a function', () => {
116+
117+
const mapStateToProps = (state, props) => ({
118+
number: state[props.keyName],
119+
test: state.flag ? state.secret : 0,
120+
subObjec: {}
121+
});
122+
123+
const Component = ({number}) => <div>{number}</div>;
124+
125+
const spy = jest.spyOn(global.console, 'error')
126+
127+
const ConnectedComponent = connectAndCheck(mapStateToProps)(Component);
128+
const wrapper = mount(
129+
<ReduxFocus focus={(state, props) => props.state} state={{key1: 'key1-value'}}>
130+
<ConnectedComponent keyName="key1"/>
131+
</ReduxFocus>
132+
);
133+
wrapper.setProps({state: {key1: 'key1-value', key2:2}});
134+
135+
expect(spy).toHaveBeenCalledWith("shouldBePure", expect.anything(),"`s result is not equal at [subObjec], while should be equal");
136+
137+
spy.mockRestore();
138+
});
139+
140+
141+
});

lib/check.js

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,8 @@ var _reactRedux = require('react-redux');
44

55
var redux = _interopRequireWildcard(_reactRedux);
66

7-
var _memoizeState = require('memoize-state');
7+
var _index = require('./index');
88

99
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
1010

11-
var c = redux.connect;
12-
redux.connect = function (a) {
13-
for (var _len = arguments.length, rest = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
14-
rest[_key - 1] = arguments[_key];
15-
}
16-
17-
return c.call.apply(c, [redux, a ? (0, _memoizeState.shouldBePure)(a) : a].concat(rest));
18-
};
11+
redux.connect = _index.connectAndCheck;

lib/index.js

Lines changed: 84 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,110 @@
33
Object.defineProperty(exports, "__esModule", {
44
value: true
55
});
6-
exports.Provider = exports.connect = undefined;
7-
8-
var _react = require('react');
6+
exports.connectAndCheck = exports.Provider = exports.connect = undefined;
97

108
var _reactRedux = require('react-redux');
119

10+
var redux = _interopRequireWildcard(_reactRedux);
11+
1212
var _memoizeState = require('memoize-state');
1313

1414
var _memoizeState2 = _interopRequireDefault(_memoizeState);
1515

1616
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
1717

18+
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
19+
1820
var realReactReduxConnect = _reactRedux.connect;
19-
var connect = function connect(mapStateToProps, mapDispatchToProps, mergeProps, options) {
21+
var connect = function connect(mapStateToProps, mapDispatchToProps, mergeProps) {
22+
var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
23+
2024

2125
if (options && 'pure' in options && !options.pure) {
2226
return realReactReduxConnect(mapStateToProps, mapDispatchToProps, mergeProps, options);
2327
}
2428

25-
var memoizedMapStateToProps = mapStateToProps && (0, _memoizeState2.default)(mapStateToProps, { strictArguments: true });
26-
2729
return function (WrappedComponent) {
2830

29-
var localMapStateToProps = memoizedMapStateToProps && mapStateToProps && (0, _memoizeState2.default)(memoizedMapStateToProps, { strictArguments: true });
31+
var lastAffectedPaths = null;
32+
var affectedMap = {};
33+
34+
var localMapStateToProps = mapStateToProps && (0, _memoizeState2.default)(mapStateToProps, { strictArity: true });
35+
36+
function mapStateToPropsFabric() {
37+
function finalMapStateToProps(state, props) {
38+
var result = localMapStateToProps(state, props);
39+
40+
if (!localMapStateToProps.cacheStatistics.lastCallWasMemoized) {
41+
// get state related paths
42+
var affected = localMapStateToProps.getAffectedPaths()[0];
43+
affected.forEach(function (key) {
44+
return affectedMap[key.split('.')[1]] = true;
45+
});
46+
lastAffectedPaths = Object.keys(affectedMap);
47+
}
48+
return result;
49+
}
50+
51+
if (localMapStateToProps) {
52+
Object.defineProperty(finalMapStateToProps, 'length', {
53+
writable: false,
54+
configurable: true,
55+
value: localMapStateToProps.length
56+
});
57+
58+
Object.defineProperty(finalMapStateToProps, 'cacheStatistics', {
59+
get: function get() {
60+
return localMapStateToProps.cacheStatistics;
61+
},
62+
configurable: true,
63+
enumerable: false
64+
});
65+
66+
Object.defineProperty(finalMapStateToProps, 'trackedKeys', {
67+
get: function get() {
68+
return lastAffectedPaths;
69+
},
70+
configurable: true,
71+
enumerable: false
72+
});
73+
}
74+
75+
return finalMapStateToProps;
76+
}
77+
78+
function areStatesEqual(state1, state2) {
79+
if (!lastAffectedPaths) {
80+
return state1 === state2;
81+
}
82+
return lastAffectedPaths.reduce(function (acc, key) {
83+
return acc && state1[key] === state2[key];
84+
}, true);
85+
}
3086

3187
// TODO: create `areStatesEqual` based on memoize-state usage.
32-
return realReactReduxConnect(localMapStateToProps, mapDispatchToProps, mergeProps, options)(WrappedComponent);
88+
var ImprovedComponent = realReactReduxConnect(localMapStateToProps && mapStateToPropsFabric, mapDispatchToProps, mergeProps, Object.assign({ areStatesEqual: areStatesEqual }, options))(WrappedComponent);
89+
90+
Object.defineProperty(ImprovedComponent, '__trackingPaths', {
91+
get: function get() {
92+
return lastAffectedPaths;
93+
},
94+
configurable: true,
95+
enumerable: false
96+
});
97+
98+
return ImprovedComponent;
3399
};
34100
};
35101

102+
var connectAndCheck = function connectAndCheck(a) {
103+
for (var _len = arguments.length, rest = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
104+
rest[_key - 1] = arguments[_key];
105+
}
106+
107+
return realReactReduxConnect.apply(undefined, [a ? (0, _memoizeState.shouldBePure)(a) : a].concat(rest));
108+
};
109+
36110
exports.connect = connect;
37-
exports.Provider = _reactRedux.Provider;
111+
exports.Provider = _reactRedux.Provider;
112+
exports.connectAndCheck = connectAndCheck;

package.json

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@
1717
"redraw"
1818
],
1919
"scripts": {
20-
"test": "npm run test:pick -- '_tests/**/*spec.js'",
21-
"test:pick": "BABEL_ENV=cjs mocha --compilers js:babel-core/register",
20+
"test": "jest",
2221
"build": "babel src -d lib",
2322
"prepublish": "npm run build"
2423
},
@@ -30,9 +29,16 @@
3029
"devDependencies": {
3130
"babel-cli": "^6.26.0",
3231
"babel-preset-env": "^1.6.1",
32+
"babel-preset-react": "^6.24.1",
3333
"chai": "^4.1.2",
34-
"mocha": "^4.0.1",
35-
"react": "^16.2.0"
34+
"enzyme": "^3.3.0",
35+
"enzyme-adapter-react-16": "^1.1.1",
36+
"jest": "^22.4.2",
37+
"react": "^16.2.0",
38+
"react-dom": "^16.2.0",
39+
"react-redux": "^5.0.7",
40+
"react-redux-focus": "^1.2.3",
41+
"redux": "^3.7.2"
3642
},
3743
"repository": {
3844
"type": "git",
@@ -43,6 +49,7 @@
4349
},
4450
"homepage": "https://github.com/theKashey/beautiful-react-redux#readme",
4551
"dependencies": {
46-
"memoize-state": "^1.1.6"
52+
"memoize-state": "^1.2.2",
53+
"shallowequal": "^1.0.2"
4754
}
4855
}

src/check.js

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import * as redux from 'react-redux';
2-
import {shouldBePure} from 'memoize-state';
2+
import {connectAndCheck} from './index';
33

4-
const c = redux.connect;
5-
redux.connect = (a, ...rest) => c.call(redux, a ? shouldBePure(a) : a, ...rest);
4+
redux.connect = connectAndCheck

0 commit comments

Comments
 (0)