Skip to content
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

New PR for mapStateToProps shorthand syntax #723

Closed
wants to merge 3 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
26 changes: 26 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ It does not modify the component class passed to it; instead, it *returns* a new
* [`mapStateToProps(state, [ownProps]): stateProps`] \(*Function*): If this argument is specified, the new component will subscribe to Redux store updates. This means that any time the store is updated, `mapStateToProps` will be called. The results of `mapStateToProps` must be a plain object, which will be merged into the component’s props. If you don't want to subscribe to store updates, pass `null` or `undefined` in place of `mapStateToProps`.

If your `mapStateToProps` function is declared as taking two parameters, it will be called with the store state as the first parameter and the props passed to the connected component as the second parameter, and will also be re-invoked whenever the connected component receives new props as determined by shallow equality comparisons. (The second parameter is normally referred to as `ownProps` by convention.)

>Note: mapStateToProps can also be passed as an user-friendly object syntax. Keys are prop names and values are selectors (or factories). This is quite similar to [createStructuredSelector](https://github.com/reactjs/reselect#createstructuredselectorinputselectors-selectorcreator--createselector) of [reselect](https://github.com/reactjs/reselect)

```javascript
Component = connect({
deviceOrientation: state => state.deviceOrientation,
user: (intialState,initialProps) => (state) => state.users[initialProps.userId],
})(Component)
```

>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.

Expand Down Expand Up @@ -274,6 +283,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
9 changes: 8 additions & 1 deletion src/connect/mapStateToProps.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { wrapMapToPropsConstant, wrapMapToPropsFunc } from './wrapMapToProps'
import { wrapMapToPropsConstant, wrapMapToPropsFunc, wrapMapStateObject } from './wrapMapToProps'

export function whenMapStateToPropsIsFunction(mapStateToProps) {
return (typeof mapStateToProps === 'function')
? wrapMapToPropsFunc(mapStateToProps, 'mapStateToProps')
: undefined
}

export function whenMapStateToPropsIsObject(mapStateToProps) {
return (typeof mapStateToProps === 'object')
? wrapMapStateObject(mapStateToProps, 'mapStateToProps')
: undefined
}

export function whenMapStateToPropsIsMissing(mapStateToProps) {
return (!mapStateToProps)
? wrapMapToPropsConstant(() => ({}))
Expand All @@ -14,5 +20,6 @@ export function whenMapStateToPropsIsMissing(mapStateToProps) {

export default [
whenMapStateToPropsIsFunction,
whenMapStateToPropsIsObject,
whenMapStateToPropsIsMissing
]
44 changes: 42 additions & 2 deletions src/connect/wrapMapToProps.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import verifyPlainObject from '../utils/verifyPlainObject'
import invariant from 'invariant'

export function wrapMapToPropsConstant(getConstant) {
return function initConstantSelector(dispatch, options) {
Expand Down Expand Up @@ -35,7 +36,7 @@ export function getDependsOnOwnProps(mapToProps) {
// * On first call, verifies the first result is a plain object, in order to warn
// the developer that their mapToProps function is not returning a valid result.
//
export function wrapMapToPropsFunc(mapToProps, methodName) {
export function wrapMapToPropsFunc(mapToProps, methodName, bypassVerifyPlainObject) {
return function initProxySelector(dispatch, { displayName }) {
const proxy = function mapToPropsProxy(stateOrDispatch, ownProps) {
return proxy.dependsOnOwnProps
Expand All @@ -57,7 +58,7 @@ export function wrapMapToPropsFunc(mapToProps, methodName) {
props = proxy(stateOrDispatch, ownProps)
}

if (process.env.NODE_ENV !== 'production')
if (process.env.NODE_ENV !== 'production' && !bypassVerifyPlainObject)
verifyPlainObject(props, displayName, methodName)

return props
Expand All @@ -66,3 +67,42 @@ export function wrapMapToPropsFunc(mapToProps, methodName) {
return proxy
}
}


function mapValues(obj, fn) {
return Object.keys(obj).reduce((result, key) => {
result[key] = fn(obj[key], key)
return result
}, {})
}

export function wrapMapStateObject(mapStateToProps, methodName) {

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)

const selectorsMapWrapped = mapValues(mapStateToProps, fn => wrapMapToPropsFunc(fn,methodName,true))

return (...args) => {

const selectorsMap = mapValues(selectorsMapWrapped,fn => fn(...args))

const mapStateToProps = needsProps
? (state, props) => mapValues(selectorsMap, fn => fn(state, props))
: state => mapValues(selectorsMap, fn => fn(state))

mapStateToProps.dependsOnOwnProps = needsProps

return mapStateToProps;
}

}
79 changes: 79 additions & 0 deletions test/components/connect.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,85 @@ describe('React', () => {
TestUtils.findRenderedComponentWithType(container, Container)
).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 pass state to given component, with shorthand factory syntax', () => {
const store = createStore(() => ({
baz: 'baz',
}))

@connect({
bazNormal: state => state.baz,
bazFactory: (intialState) => (state) => intialState.baz + state.baz,
bazFactoryWithProps: (intialState, initialProps) => (state, props) => intialState.baz + initialProps.pass + state.baz + props.pass
})
class Container extends Component {
render() {
return <Passthrough {...this.props} />
}
}

const container = TestUtils.renderIntoDocument(
<ProviderMock store={store}>
<Container pass="through"/>
</ProviderMock>
)
const stub = TestUtils.findRenderedComponentWithType(container, Passthrough)
expect(stub.props.pass).toEqual('through')
expect(stub.props.bazNormal).toEqual('baz')
expect(stub.props.bazFactory).toEqual("bazbaz")
expect(stub.props.bazFactoryWithProps).toEqual('bazthroughbazthrough')
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