Skip to content

Support mapStateToTarget thunks #89

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 3, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:*
Expand All @@ -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)



Expand Down
30 changes: 22 additions & 8 deletions src/components/connector.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) :
Expand All @@ -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);

Expand Down Expand Up @@ -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;
}
17 changes: 16 additions & 1 deletion test/components/connector.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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);
Expand Down