diff --git a/docs/api.md b/docs/api.md index edbe3b0db..b14ba3955 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. @@ -239,6 +241,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 diff --git a/src/components/connect.js b/src/components/connect.js index 3b60ebbce..29555f02f 100644 --- a/src/components/connect.js +++ b/src/components/connect.js @@ -3,6 +3,7 @@ import storeShape from '../utils/storeShape' import shallowEqual from '../utils/shallowEqual' import wrapActionCreators from '../utils/wrapActionCreators' import warning from '../utils/warning' +import wrapMapStateObject from '../utils/wrapMapStateObject' import isPlainObject from 'lodash/isPlainObject' import hoistStatics from 'hoist-non-react-statics' import invariant from 'invariant' @@ -32,9 +33,18 @@ function tryCatch(fn, ctx) { // Helps track hot reloading. let nextVersion = 0 +function handleShorthandSyntax(mapStateToProps) { + if ( mapStateToProps !== null && typeof mapStateToProps === 'object' ) { + return wrapMapStateObject(mapStateToProps) + } + else { + return mapStateToProps + } +} + export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) { const shouldSubscribe = Boolean(mapStateToProps) - const mapState = mapStateToProps || defaultMapStateToProps + const mapState = handleShorthandSyntax(mapStateToProps) || defaultMapStateToProps let mapDispatch if (typeof mapDispatchToProps === 'function') { diff --git a/src/utils/wrapMapStateObject.js b/src/utils/wrapMapStateObject.js new file mode 100644 index 000000000..d1d4a13c8 --- /dev/null +++ b/src/utils/wrapMapStateObject.js @@ -0,0 +1,29 @@ +import invariant from 'invariant' + + +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 514eea018..37d2c2451 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( + /mapStateToProps object key baz expected to be a function, instead saw string/ + ) + }) + it('should subscribe class components to the store changes', () => { const store = createStore(stringBuilder)