Skip to content

Commit 86eb59b

Browse files
authored
Merge pull request #89 from troch/memoization
Support mapStateToTarget thunks
2 parents 011bdc4 + dda4d2d commit 86eb59b

File tree

3 files changed

+40
-11
lines changed

3 files changed

+40
-11
lines changed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ Creates the Redux store, and allow `connect()` to access it.
131131
Connects an Angular component to Redux.
132132

133133
#### Arguments
134-
* `mapStateToTarget` \(*Function*): connect will subscribe to Redux store updates. Any time it updates, mapStateToTarget will be called. Its result must be a plain object, and it will be merged into `target`. If you have a component which simply triggers actions without needing any state you can pass null to `mapStateToTarget`.
134+
* `mapStateToTarget` \(*Function*): connect will subscribe to Redux store updates. Any time it updates, mapStateToTarget will be called. Its result must be a plain object or a function returning a plaing object, and it will be merged into `target`. If you have a component which simply triggers actions without needing any state you can pass null to `mapStateToTarget`.
135135
* [`mapDispatchToTarget`] \(*Object* or *Function*): Optional. If an object is passed, each function inside it will be assumed to be a Redux action creator. An object with the same function names, but bound to a Redux store, will be merged onto `target`. If a function is passed, it will be given `dispatch`. It’s up to you to return an object that somehow uses `dispatch` to bind action creators in your own way. (Tip: you may use the [`bindActionCreators()`](http://gaearon.github.io/redux/docs/api/bindActionCreators.html) helper from Redux.).
136136

137137
*You then need to invoke the function a second time, with `target` as parameter:*
@@ -148,7 +148,7 @@ connect(this.mapState, this.mapDispatch)((selectedState, actions) => {/* ... */}
148148
Returns a *Function* allowing to unsubscribe from further store updates.
149149

150150
#### Remarks
151-
* The `mapStateToTarget` function takes a single argument of the entire Redux store’s state and returns an object to be passed as props. It is often called a selector. Use reselect to efficiently compose selectors and compute derived data.
151+
* The `mapStateToTarget` function takes a single argument of the entire Redux store’s state and returns an object to be passed as props. It is often called a selector. Use reselect to efficiently compose selectors and compute derived data. You can also choose to use per-instance memoization by having a `mapStateToTarget` function returning a function of state, see [Sharing selectors across multiple components](https://github.com/reactjs/reselect#user-content-sharing-selectors-with-props-across-multiple-components)
152152

153153

154154

src/components/connector.js

+22-8
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const defaultMapDispatchToTarget = dispatch => ({dispatch});
1313
export default function Connector(store) {
1414
return (mapStateToTarget, mapDispatchToTarget) => {
1515

16-
const finalMapStateToTarget = mapStateToTarget || defaultMapStateToTarget;
16+
let finalMapStateToTarget = mapStateToTarget || defaultMapStateToTarget;
1717

1818
const finalMapDispatchToTarget = isPlainObject(mapDispatchToTarget) ?
1919
wrapActionCreators(mapDispatchToTarget) :
@@ -29,7 +29,13 @@ export default function Connector(store) {
2929
'mapDispatchToTarget must be a plain Object or a Function. Instead received %s.', finalMapDispatchToTarget
3030
);
3131

32-
let slice = getStateSlice(store.getState(), finalMapStateToTarget);
32+
let slice = getStateSlice(store.getState(), finalMapStateToTarget, false);
33+
const isFactory = isFunction(slice);
34+
35+
if (isFactory) {
36+
finalMapStateToTarget = slice;
37+
slice = getStateSlice(store.getState(), finalMapStateToTarget);
38+
}
3339

3440
const boundActionCreators = finalMapDispatchToTarget(store.dispatch);
3541

@@ -64,14 +70,22 @@ function updateTarget(target, StateSlice, dispatch) {
6470
}
6571
}
6672

67-
function getStateSlice(state, mapStateToScope) {
73+
function getStateSlice(state, mapStateToScope, shouldReturnObject = true) {
6874
const slice = mapStateToScope(state);
6975

70-
invariant(
71-
isPlainObject(slice),
72-
'`mapStateToScope` must return an object. Instead received %s.',
73-
slice
74-
);
76+
if (shouldReturnObject) {
77+
invariant(
78+
isPlainObject(slice),
79+
'`mapStateToScope` must return an object. Instead received %s.',
80+
slice
81+
);
82+
} else {
83+
invariant(
84+
isPlainObject(slice) || isFunction(slice),
85+
'`mapStateToScope` must return an object or a function. Instead received %s.',
86+
slice
87+
);
88+
}
7589

7690
return slice;
7791
}

test/components/connector.spec.js

+16-1
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@ describe('Connector', () => {
3131
expect(connect(() => ({})).bind(connect, () => {})).toNotThrow();
3232
});
3333

34-
it('Should throw when selector does not return a plain object', () => {
34+
it('Should throw when selector does not return a plain object or a function', () => {
3535
expect(connect.bind(connect, state => state.foo)).toThrow();
36+
expect(connect.bind(connect, state => state => state.foo)).toThrow();
3637
});
3738

3839
it('Should extend target (Object) with selected state once directly after creation', () => {
@@ -67,6 +68,20 @@ describe('Connector', () => {
6768

6869
});
6970

71+
it('should update the target (Object) if a function is returned instead of an object', () => {
72+
connect(state => state => state)(targetObj);
73+
store.dispatch({ type: 'ACTION', payload: 5 });
74+
75+
expect(targetObj.baz).toBe(5);
76+
77+
targetObj.baz = 0;
78+
79+
//this should not replace our mutation, since the state didn't change
80+
store.dispatch({ type: 'ACTION', payload: 5 });
81+
82+
expect(targetObj.baz).toBe(0);
83+
});
84+
7085
it('Should extend target (object) with actionCreators', () => {
7186
connect(() => ({}), { ac1: () => { }, ac2: () => { } })(targetObj);
7287
expect(isFunction(targetObj.ac1)).toBe(true);

0 commit comments

Comments
 (0)