Skip to content

Commit

Permalink
Supporting factory selectors in the object mapToProps argument
Browse files Browse the repository at this point in the history
  • Loading branch information
josepot committed Jan 23, 2018
1 parent 76dd7fa commit 0fbe4fe
Show file tree
Hide file tree
Showing 5 changed files with 280 additions and 55 deletions.
37 changes: 34 additions & 3 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,12 @@ It does not modify the component class passed to it; instead, it *returns* a new
<a id="connect-arguments"></a>
#### Arguments

* [`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`.
* [`mapStateToProps(state, [ownProps]): stateProps`] \(*Function* or *Object*): 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.)

If an object is passed, each function inside it is assumed to be a Redux selector (or -for advanced scenarios- a Factory function, see below). An object with the same keys, but with the result of invoking each one of its values like the normal `mapStateToProps` function, will be merged into the component’s props.

>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.
>The `mapStateToProps` function's first argument is the entire Redux store’s state and it returns an object to be passed as props. It is often called a **selector**. Use [reselect](https://github.com/reactjs/reselect) to efficiently compose selectors and [compute derived data](http://redux.js.org/docs/recipes/ComputingDerivedData.html).
Expand Down Expand Up @@ -222,8 +224,8 @@ export default connect(mapStateToProps, mapDispatchToProps)(TodoApp)
```js
import { addTodo, deleteTodo } from './actionCreators'

function mapStateToProps(state) {
return { todos: state.todos }
const mapStateToProps = {
todos: state => state.todos
}

const mapDispatchToProps = {
Expand Down Expand Up @@ -357,6 +359,35 @@ function mapDispatchToPropsFactory(initialState, initialProps) {
export default connect(mapStateToPropsFactory, mapDispatchToPropsFactory)(TodoApp)
```

The object shorthand of `stateToProps` also accepts Factory functions

```js
import { addTodo } from './actionCreators'

const always = x => () => x;

const mapStateToProps = {
anotherProperty: (initialState, initialProps) =>
always(200 + initialState[initialProps.another]),
someProperty: (initialState, initilProps) => createSelector(...),
todos: state => state.todos,
}

function mapDispatchToPropsFactory(initialState, initialProps) {
function goToSomeLink(){
initialProps.history.push('some/link');
}
return function(dispatch){
return {
addTodo
}
}
}


export default connect(mapStateToProps, mapDispatchToPropsFactory)(TodoApp)
```

<a id="connectAdvanced"></a>
### `connectAdvanced(selectorFactory, [connectOptions])`

Expand Down
34 changes: 17 additions & 17 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 19 additions & 1 deletion src/connect/mapStateToProps.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,28 @@
import { wrapMapToPropsConstant, wrapMapToPropsFunc } from './wrapMapToProps'
import {
wrapMapToPropsConstant,
wrapMapToPropsFunc,
wrapMapToPropsObject
} from './wrapMapToProps'

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

function isValidmapStateToPropsObj(mapStateToProps) {
return typeof mapStateToProps === 'object' && Object
.keys(mapStateToProps)
.map(function (key) { return mapStateToProps[key] })
.every(function (val) { return typeof val === 'function' })
}

export function whenMapStateToPropsIsObject(mapStateToProps) {
return (isValidmapStateToPropsObj(mapStateToProps)) ?
wrapMapToPropsObject(mapStateToProps) :
undefined
}

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

export default [
whenMapStateToPropsIsFunction,
whenMapStateToPropsIsObject,
whenMapStateToPropsIsMissing
]
40 changes: 38 additions & 2 deletions src/connect/wrapMapToProps.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,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, isObjMapToProps = false) {
return function initProxySelector(dispatch, { displayName }) {
const proxy = function mapToPropsProxy(stateOrDispatch, ownProps) {
return proxy.dependsOnOwnProps
Expand All @@ -57,7 +57,7 @@ export function wrapMapToPropsFunc(mapToProps, methodName) {
props = proxy(stateOrDispatch, ownProps)
}

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

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

function mapObject (obj, mapFn) {
const result = {}
Object
.keys(obj)
.forEach(function(key) { result[key] = mapFn(obj[key], key, obj) })
return result
}

export function wrapMapToPropsObject (mapStateToProps) {
const wrappedMapToProps = mapObject(mapStateToProps, function (fn) {
return wrapMapToPropsFunc(fn, 'mapStateToProps', true)
})

const dependsOnOwnProps = Object
.keys(wrappedMapToProps)
.map(key => wrappedMapToProps[key])
.some(getDependsOnOwnProps)

function initObjectSelector(...initArgs) {
const initializedWraps = mapObject(wrappedMapToProps, function (fn) {
return fn(...initArgs)
})

function objectSelector (...selectorArgs) {
return mapObject(initializedWraps, function (fn) {
return fn(...selectorArgs)
})
}

objectSelector.dependsOnOwnProps = dependsOnOwnProps
return objectSelector
}

return initObjectSelector
}
Loading

0 comments on commit 0fbe4fe

Please sign in to comment.