Skip to content

[RFC] Add shorthand syntax for mapStateToProps #323

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

Closed
wants to merge 7 commits into from
Closed
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
19 changes: 19 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
12 changes: 11 additions & 1 deletion src/components/connect.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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') {
Expand Down
29 changes: 29 additions & 0 deletions src/utils/wrapMapStateObject.js
Original file line number Diff line number Diff line change
@@ -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)
)
}
48 changes: 48 additions & 0 deletions test/components/connect.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <Passthrough {...this.props} />
}
}

const container = TestUtils.renderIntoDocument(
<ProviderMock store={store}>
<Container pass="through" baz={50} />
</ProviderMock>
)
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 <div/>
}
}
).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)

Expand Down