From 4ebf2088867e952fd79fdd92d2b9a572cf38a6b6 Mon Sep 17 00:00:00 2001 From: sebastien Date: Thu, 17 Mar 2016 19:11:57 +0100 Subject: [PATCH 1/6] Implement connect() shorthand syntax --- src/components/connect.js | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/components/connect.js b/src/components/connect.js index 5840d7e17..0126ccd94 100644 --- a/src/components/connect.js +++ b/src/components/connect.js @@ -31,9 +31,37 @@ function checkStateShape(stateProps, dispatch) { // Helps track hot reloading. let nextVersion = 0 + +function mapValues(obj, fn) { + return Object.keys(obj).reduce((result, key) => { + result[key] = fn(obj[key], key) + return result + }, {}) +} + +function handleShorthandSyntax(mapStateToProps) { + if ( mapStateToProps !== null && typeof mapStateToProps === 'object' ) { + Object.keys(mapStateToProps).forEach(key => { + if ( typeof mapStateToProps[key] !== 'function' ) { + throw new Error('You are using the shorthand mapStateToProps syntax. ' + + 'All the shorthand keys should be associated to a function (\"selector\") that receive state as parameter. Bad key='+key) + } + }) + return state => { + return mapValues(mapStateToProps,stateSelectorFn => { + return stateSelectorFn(state) + }) + } + } + else { + return mapStateToProps + } +} + + export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) { const shouldSubscribe = Boolean(mapStateToProps) - const mapState = mapStateToProps || defaultMapStateToProps + const mapState = handleShorthandSyntax(mapStateToProps) || defaultMapStateToProps const mapDispatch = isPlainObject(mapDispatchToProps) ? wrapActionCreators(mapDispatchToProps) : mapDispatchToProps || defaultMapDispatchToProps From afd7fd50c78f0cb51901e8721dda468c3c473a54 Mon Sep 17 00:00:00 2001 From: sebastien Date: Thu, 17 Mar 2016 19:12:39 +0100 Subject: [PATCH 2/6] connect shorthand syntax tests --- test/components/connect.spec.js | 48 +++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index 82a1c3ef5..3e8c373c6 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -82,6 +82,54 @@ describe('React', () => { ).toNotThrow() }) + it('should pass state to given component, with shorthand syntax', () => { + const store = createStore(() => ({ + foo: 'bar', + baz: 42, + hello: 'world' + })) + + @connect({ + foo: state => state.foo, + baz: state => state.baz + }) + class Container extends Component { + render() { + return + } + } + + const container = TestUtils.renderIntoDocument( + + + + ) + const stub = TestUtils.findRenderedComponentWithType(container, Passthrough) + expect(stub.props.pass).toEqual('through') + expect(stub.props.foo).toEqual('bar') + expect(stub.props.baz).toEqual(42) + expect(stub.props.hello).toEqual(undefined) + expect(() => + TestUtils.findRenderedComponentWithType(container, Container) + ).toNotThrow() + }) + + it('should throw error if connect is called with shorthand syntax and one object value is not a function', () => { + expect(() => + @connect({ + foo: state => state.foo, + baz: "badValue" + }) + class Container extends Component { + render() { + return
+ } + } + ).toThrow( + /All the shorthand keys should be associated to a function/ + ) + }) + it('should subscribe class components to the store changes', () => { const store = createStore(stringBuilder) From ae4c7a016183e947a2862e1191db8c4ade0670b5 Mon Sep 17 00:00:00 2001 From: sebastien Date: Thu, 17 Mar 2016 19:13:15 +0100 Subject: [PATCH 3/6] Fix lint issue --- test/components/connect.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index 3e8c373c6..d1e5694b4 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -118,7 +118,7 @@ describe('React', () => { expect(() => @connect({ foo: state => state.foo, - baz: "badValue" + baz: 'badValue' }) class Container extends Component { render() { From 450e48964fcf1a12cacccb1737b0c3d5ff9e6ef5 Mon Sep 17 00:00:00 2001 From: sebastien Date: Thu, 17 Mar 2016 19:23:07 +0100 Subject: [PATCH 4/6] mention shorthand syntax in doc --- docs/api.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/api.md b/docs/api.md index bdefef11c..f514c0d4d 100644 --- a/docs/api.md +++ b/docs/api.md @@ -60,6 +60,8 @@ Instead, it *returns* a new, connected component class, for you to use. * [`mapStateToProps(state, [ownProps]): stateProps`] \(*Function*): If specified, the component will subscribe to Redux store updates. Any time it updates, `mapStateToProps` will be called. Its result must be a plain object*, and it will be merged into the component’s props. If you omit it, the component will not be subscribed to the Redux store. If `ownProps` is specified as a second argument, its value will be the props passed to your component, and `mapStateToProps` will be re-invoked whenever the component receives new props. + >Note: if your mapStateToProps do not need to access component properties, you can use a shorthand syntax by passing an object whose values are "selectors" (See [reselect](https://github.com/reactjs/reselect)) + >Note: in advanced scenarios where you need more control over the rendering performance, `mapStateToProps()` can also return a function. In this case, *that* function will be used as `mapStateToProps()` for a particular component instance. This allows you to do per-instance memoization. You can refer to [#279](https://github.com/reactjs/react-redux/pull/279) and the tests it adds for more details. Most apps never need this. * [`mapDispatchToProps(dispatch, [ownProps]): dispatchProps`] \(*Object* or *Function*): 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 with every action creator wrapped into a `dispatch` call so they may be invoked directly, will be merged into the component’s props. 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://reactjs.github.io/redux/docs/api/bindActionCreators.html) helper from Redux.) If you omit it, the default implementation just injects `dispatch` into your component’s props. If `ownProps` is specified as a second argument, its value will be the props passed to your component, and `mapDispatchToProps` will be re-invoked whenever the component receives new props. From 39ef732105f41c9b9b1eb0139a7dbaf93dd7e144 Mon Sep 17 00:00:00 2001 From: sebastien Date: Thu, 17 Mar 2016 19:26:02 +0100 Subject: [PATCH 5/6] Add documentation for shorthand syntax --- docs/api.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/api.md b/docs/api.md index f514c0d4d..ed2e82c9e 100644 --- a/docs/api.md +++ b/docs/api.md @@ -233,6 +233,23 @@ function mapDispatchToProps(dispatch) { export default connect(mapStateToProps, mapDispatchToProps)(TodoApp) ``` +##### Inject `todos` with shorthand syntax, and all todoActionCreators and counterActionCreators directly as props + +```js +import * as todoActionCreators from './todoActionCreators' +import * as counterActionCreators from './counterActionCreators' +import { bindActionCreators } from 'redux' + + +const todosSelector = state => state.todos + +function mapDispatchToProps(dispatch) { + return bindActionCreators(Object.assign({}, todoActionCreators, counterActionCreators), dispatch) +} + +export default connect({todos: todosSelector}, mapDispatchToProps)(TodoApp) +``` + ##### Inject `todos` of a specific user depending on props ```js From ed64343eda77790a1b35e28237da54894458d34a Mon Sep 17 00:00:00 2001 From: sebastien Date: Sat, 23 Apr 2016 15:33:06 +0200 Subject: [PATCH 6/6] Update PR for mapState shorthand syntax with @tgriesser suggestions --- src/utils/wrapMapStateObject.js | 30 +++++++++++++++++++++++++++--- test/components/connect.spec.js | 2 +- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/utils/wrapMapStateObject.js b/src/utils/wrapMapStateObject.js index 349e03d0f..d1d4a13c8 100644 --- a/src/utils/wrapMapStateObject.js +++ b/src/utils/wrapMapStateObject.js @@ -1,5 +1,29 @@ -import { bindActionCreators } from 'redux' +import invariant from 'invariant' -export default function wrapActionCreators(actionCreators) { - return dispatch => bindActionCreators(actionCreators, dispatch) + +function mapValues(obj, fn) { + return Object.keys(obj).reduce((result, key) => { + result[key] = fn(obj[key], key) + return result + }, {}) +} + +export default function wrapMapStateObject(mapStateToProps) { + + const needsProps = Object.keys(mapStateToProps) + .reduce((useProps, key) => { + const type = typeof mapStateToProps[key] + invariant( + type === 'function', + 'mapStateToProps object key %s expected to be a function, instead saw %s', + key, + type + ) + return useProps || mapStateToProps[key].length !== 1 + }, false) + + return needsProps + ? (state, props) => mapValues(mapStateToProps, fn => fn(state, props)) + : state => mapValues(mapStateToProps, fn => fn(state) + ) } diff --git a/test/components/connect.spec.js b/test/components/connect.spec.js index ea8cd8ac1..37d2c2451 100644 --- a/test/components/connect.spec.js +++ b/test/components/connect.spec.js @@ -126,7 +126,7 @@ describe('React', () => { } } ).toThrow( - /All the shorthand keys should be associated to a function/ + /mapStateToProps object key baz expected to be a function, instead saw string/ ) })