diff --git a/README.md b/README.md index 45f0fe0..8f9b047 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,7 @@ Creates the Redux store, and allow `connect()` to access it. Connects an Angular component to Redux. #### Arguments -* `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`. +* `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`. * [`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.). *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) => {/* ... */} Returns a *Function* allowing to unsubscribe from further store updates. #### Remarks -* 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. +* 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) diff --git a/src/components/connector.js b/src/components/connector.js index 949c7b3..ac00724 100644 --- a/src/components/connector.js +++ b/src/components/connector.js @@ -13,7 +13,7 @@ const defaultMapDispatchToTarget = dispatch => ({dispatch}); export default function Connector(store) { return (mapStateToTarget, mapDispatchToTarget) => { - const finalMapStateToTarget = mapStateToTarget || defaultMapStateToTarget; + let finalMapStateToTarget = mapStateToTarget || defaultMapStateToTarget; const finalMapDispatchToTarget = isPlainObject(mapDispatchToTarget) ? wrapActionCreators(mapDispatchToTarget) : @@ -29,7 +29,13 @@ export default function Connector(store) { 'mapDispatchToTarget must be a plain Object or a Function. Instead received %s.', finalMapDispatchToTarget ); - let slice = getStateSlice(store.getState(), finalMapStateToTarget); + let slice = getStateSlice(store.getState(), finalMapStateToTarget, false); + const isFactory = isFunction(slice); + + if (isFactory) { + finalMapStateToTarget = slice; + slice = getStateSlice(store.getState(), finalMapStateToTarget); + } const boundActionCreators = finalMapDispatchToTarget(store.dispatch); @@ -64,14 +70,22 @@ function updateTarget(target, StateSlice, dispatch) { } } -function getStateSlice(state, mapStateToScope) { +function getStateSlice(state, mapStateToScope, shouldReturnObject = true) { const slice = mapStateToScope(state); - invariant( - isPlainObject(slice), - '`mapStateToScope` must return an object. Instead received %s.', - slice - ); + if (shouldReturnObject) { + invariant( + isPlainObject(slice), + '`mapStateToScope` must return an object. Instead received %s.', + slice + ); + } else { + invariant( + isPlainObject(slice) || isFunction(slice), + '`mapStateToScope` must return an object or a function. Instead received %s.', + slice + ); + } return slice; } diff --git a/test/components/connector.spec.js b/test/components/connector.spec.js index 7d2265a..780b8e4 100644 --- a/test/components/connector.spec.js +++ b/test/components/connector.spec.js @@ -31,8 +31,9 @@ describe('Connector', () => { expect(connect(() => ({})).bind(connect, () => {})).toNotThrow(); }); - it('Should throw when selector does not return a plain object', () => { + it('Should throw when selector does not return a plain object or a function', () => { expect(connect.bind(connect, state => state.foo)).toThrow(); + expect(connect.bind(connect, state => state => state.foo)).toThrow(); }); it('Should extend target (Object) with selected state once directly after creation', () => { @@ -67,6 +68,20 @@ describe('Connector', () => { }); + it('should update the target (Object) if a function is returned instead of an object', () => { + connect(state => state => state)(targetObj); + store.dispatch({ type: 'ACTION', payload: 5 }); + + expect(targetObj.baz).toBe(5); + + targetObj.baz = 0; + + //this should not replace our mutation, since the state didn't change + store.dispatch({ type: 'ACTION', payload: 5 }); + + expect(targetObj.baz).toBe(0); + }); + it('Should extend target (object) with actionCreators', () => { connect(() => ({}), { ac1: () => { }, ac2: () => { } })(targetObj); expect(isFunction(targetObj.ac1)).toBe(true);