Skip to content

Commit 43344c9

Browse files
committed
Support factory functions in connect mapToProps args
1 parent 22cf67b commit 43344c9

File tree

2 files changed

+180
-53
lines changed

2 files changed

+180
-53
lines changed

src/components/connect.js

+77-53
Original file line numberDiff line numberDiff line change
@@ -18,61 +18,32 @@ function getDisplayName(WrappedComponent) {
1818
return WrappedComponent.displayName || WrappedComponent.name || 'Component'
1919
}
2020

21+
function checkStateShape(stateProps, dispatch) {
22+
invariant(
23+
isPlainObject(stateProps),
24+
'`%sToProps` must return an object. Instead received %s.',
25+
dispatch ? 'mapDispatch' : 'mapState',
26+
stateProps
27+
)
28+
return stateProps
29+
}
30+
2131
// Helps track hot reloading.
2232
let nextVersion = 0
2333

2434
export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) {
2535
const shouldSubscribe = Boolean(mapStateToProps)
26-
const finalMapStateToProps = mapStateToProps || defaultMapStateToProps
27-
const finalMapDispatchToProps = isPlainObject(mapDispatchToProps) ?
36+
const mapState = mapStateToProps || defaultMapStateToProps
37+
const mapDispatch = isPlainObject(mapDispatchToProps) ?
2838
wrapActionCreators(mapDispatchToProps) :
2939
mapDispatchToProps || defaultMapDispatchToProps
40+
3041
const finalMergeProps = mergeProps || defaultMergeProps
31-
const doStatePropsDependOnOwnProps = finalMapStateToProps.length !== 1
32-
const doDispatchPropsDependOnOwnProps = finalMapDispatchToProps.length !== 1
3342
const { pure = true, withRef = false } = options
3443

3544
// Helps track hot reloading.
3645
const version = nextVersion++
3746

38-
function computeStateProps(store, props) {
39-
const state = store.getState()
40-
const stateProps = doStatePropsDependOnOwnProps ?
41-
finalMapStateToProps(state, props) :
42-
finalMapStateToProps(state)
43-
44-
invariant(
45-
isPlainObject(stateProps),
46-
'`mapStateToProps` must return an object. Instead received %s.',
47-
stateProps
48-
)
49-
return stateProps
50-
}
51-
52-
function computeDispatchProps(store, props) {
53-
const { dispatch } = store
54-
const dispatchProps = doDispatchPropsDependOnOwnProps ?
55-
finalMapDispatchToProps(dispatch, props) :
56-
finalMapDispatchToProps(dispatch)
57-
58-
invariant(
59-
isPlainObject(dispatchProps),
60-
'`mapDispatchToProps` must return an object. Instead received %s.',
61-
dispatchProps
62-
)
63-
return dispatchProps
64-
}
65-
66-
function computeMergedProps(stateProps, dispatchProps, parentProps) {
67-
const mergedProps = finalMergeProps(stateProps, dispatchProps, parentProps)
68-
invariant(
69-
isPlainObject(mergedProps),
70-
'`mergeProps` must return an object. Instead received %s.',
71-
mergedProps
72-
)
73-
return mergedProps
74-
}
75-
7647
return function wrapWithConnect(WrappedComponent) {
7748
class Connect extends Component {
7849
shouldComponentUpdate() {
@@ -96,8 +67,57 @@ export default function connect(mapStateToProps, mapDispatchToProps, mergeProps,
9667
this.clearCache()
9768
}
9869

70+
computeStateProps(store, props) {
71+
if (!this.finalMapStateToProps) {
72+
return this.configureFinalMapState(store, props)
73+
}
74+
const state = store.getState()
75+
const stateProps = this.doStatePropsDependOnOwnProps ?
76+
this.finalMapStateToProps(state, props) :
77+
this.finalMapStateToProps(state)
78+
79+
return checkStateShape(stateProps)
80+
}
81+
82+
configureFinalMapState(store, props) {
83+
const mappedState = mapState(store.getState(), props)
84+
const isFactory = typeof mappedState === 'function'
85+
this.finalMapStateToProps = isFactory ? mappedState : mapState
86+
this.doStatePropsDependOnOwnProps = this.finalMapStateToProps.length !== 1
87+
return isFactory ? this.computeStateProps(store, props) : checkStateShape(mappedState)
88+
}
89+
90+
computeDispatchProps(store, props) {
91+
if (!this.finalMapDispatchToProps) {
92+
return this.configureFinalMapDispatch(store, props)
93+
}
94+
const { dispatch } = store
95+
const dispatchProps = this.doDispatchPropsDependOnOwnProps ?
96+
this.finalMapDispatchToProps(dispatch, props) :
97+
this.finalMapDispatchToProps(dispatch)
98+
return checkStateShape(dispatchProps, true)
99+
}
100+
101+
configureFinalMapDispatch(store, props) {
102+
const mappedDispatch = mapDispatch(store.dispatch, props)
103+
const isFactory = typeof mappedDispatch === 'function'
104+
this.finalMapDispatchToProps = isFactory ? mappedDispatch : mapDispatch
105+
this.doDispatchPropsDependOnOwnProps = this.finalMapDispatchToProps.length !== 1
106+
return isFactory ? this.computeDispatchProps(store, props) : checkStateShape(mappedDispatch, true)
107+
}
108+
109+
computeMergedProps(stateProps, dispatchProps, parentProps) {
110+
const mergedProps = finalMergeProps(stateProps, dispatchProps, parentProps)
111+
invariant(
112+
isPlainObject(mergedProps),
113+
'`mergeProps` must return an object. Instead received %s.',
114+
mergedProps
115+
)
116+
return mergedProps
117+
}
118+
99119
updateStatePropsIfNeeded() {
100-
const nextStateProps = computeStateProps(this.store, this.props)
120+
const nextStateProps = this.computeStateProps(this.store, this.props)
101121
if (this.stateProps && shallowEqual(nextStateProps, this.stateProps)) {
102122
return false
103123
}
@@ -107,7 +127,7 @@ export default function connect(mapStateToProps, mapDispatchToProps, mergeProps,
107127
}
108128

109129
updateDispatchPropsIfNeeded() {
110-
const nextDispatchProps = computeDispatchProps(this.store, this.props)
130+
const nextDispatchProps = this.computeDispatchProps(this.store, this.props)
111131
if (this.dispatchProps && shallowEqual(nextDispatchProps, this.dispatchProps)) {
112132
return false
113133
}
@@ -116,12 +136,14 @@ export default function connect(mapStateToProps, mapDispatchToProps, mergeProps,
116136
return true
117137
}
118138

119-
updateMergedProps() {
120-
this.mergedProps = computeMergedProps(
121-
this.stateProps,
122-
this.dispatchProps,
123-
this.props
124-
)
139+
updateMergedPropsIfNeeded() {
140+
const nextMergedProps = this.computeMergedProps(this.stateProps, this.dispatchProps, this.props)
141+
if (this.mergedProps && shallowEqual(nextMergedProps, this.mergedProps)) {
142+
return false
143+
}
144+
145+
this.mergedProps = nextMergedProps
146+
return true
125147
}
126148

127149
isSubscribed() {
@@ -164,6 +186,8 @@ export default function connect(mapStateToProps, mapDispatchToProps, mergeProps,
164186
this.haveOwnPropsChanged = true
165187
this.hasStoreStateChanged = true
166188
this.renderedElement = null
189+
this.finalMapDispatchToProps = null
190+
this.finalMapStateToProps = null
167191
}
168192

169193
handleChange() {
@@ -203,10 +227,10 @@ export default function connect(mapStateToProps, mapDispatchToProps, mergeProps,
203227
let shouldUpdateDispatchProps = true
204228
if (pure && renderedElement) {
205229
shouldUpdateStateProps = hasStoreStateChanged || (
206-
haveOwnPropsChanged && doStatePropsDependOnOwnProps
230+
haveOwnPropsChanged && this.doStatePropsDependOnOwnProps
207231
)
208232
shouldUpdateDispatchProps =
209-
haveOwnPropsChanged && doDispatchPropsDependOnOwnProps
233+
haveOwnPropsChanged && this.doDispatchPropsDependOnOwnProps
210234
}
211235

212236
let haveStatePropsChanged = false
@@ -224,7 +248,7 @@ export default function connect(mapStateToProps, mapDispatchToProps, mergeProps,
224248
haveDispatchPropsChanged ||
225249
haveOwnPropsChanged
226250
) {
227-
this.updateMergedProps()
251+
haveMergedPropsChanged = this.updateMergedPropsIfNeeded()
228252
} else {
229253
haveMergedPropsChanged = false
230254
}

test/components/connect.spec.js

+103
Original file line numberDiff line numberDiff line change
@@ -1509,5 +1509,108 @@ describe('React', () => {
15091509
// But render is not because it did not make any actual changes
15101510
expect(renderCalls).toBe(1)
15111511
})
1512+
1513+
it('should allow providing a factory function to mapStateToProps', () => {
1514+
let updatedCount = 0
1515+
let memoizedReturnCount = 0
1516+
const store = createStore(() => ({ value: 1 }))
1517+
1518+
const mapStateFactory = () => {
1519+
let lastProp, lastVal, lastResult
1520+
return (state, props) => {
1521+
if (props.name === lastProp && lastVal === state.value) {
1522+
memoizedReturnCount++
1523+
return lastResult
1524+
}
1525+
lastProp = props.name
1526+
lastVal = state.value
1527+
return lastResult = { someObject: { prop: props.name, stateVal: state.value } }
1528+
}
1529+
}
1530+
1531+
@connect(mapStateFactory)
1532+
class Container extends Component {
1533+
componentWillUpdate() {
1534+
updatedCount++
1535+
}
1536+
render() {
1537+
return <div {...this.props} />
1538+
}
1539+
}
1540+
1541+
TestUtils.renderIntoDocument(
1542+
<ProviderMock store={store}>
1543+
<div>
1544+
<Container name="a" />
1545+
<Container name="b" />
1546+
</div>
1547+
</ProviderMock>
1548+
)
1549+
1550+
store.dispatch({ type: 'test' })
1551+
expect(updatedCount).toBe(0)
1552+
expect(memoizedReturnCount).toBe(2)
1553+
})
1554+
1555+
it('should allow providing a factory function to mapDispatchToProps', () => {
1556+
let updatedCount = 0
1557+
let memoizedReturnCount = 0
1558+
const store = createStore(() => ({ value: 1 }))
1559+
1560+
const mapDispatchFactory = () => {
1561+
let lastProp, lastResult
1562+
return (dispatch, props) => {
1563+
if (props.name === lastProp) {
1564+
memoizedReturnCount++
1565+
return lastResult
1566+
}
1567+
lastProp = props.name
1568+
return lastResult = { someObject: { dispatchFn: dispatch } }
1569+
}
1570+
}
1571+
function mergeParentDispatch(stateProps, dispatchProps, parentProps) {
1572+
return { ...stateProps, ...dispatchProps, name: parentProps.name }
1573+
}
1574+
1575+
@connect(null, mapDispatchFactory, mergeParentDispatch)
1576+
class Passthrough extends Component {
1577+
componentWillUpdate() {
1578+
updatedCount++
1579+
}
1580+
render() {
1581+
return <div {...this.props} />
1582+
}
1583+
}
1584+
1585+
class Container extends Component {
1586+
constructor(props) {
1587+
super(props)
1588+
this.state = { count: 0 }
1589+
}
1590+
componentDidMount() {
1591+
this.setState({ count: 1 })
1592+
}
1593+
render() {
1594+
const { count } = this.state
1595+
return (
1596+
<div>
1597+
<Passthrough count={count} name="a" />
1598+
<Passthrough count={count} name="b" />
1599+
</div>
1600+
)
1601+
}
1602+
}
1603+
1604+
TestUtils.renderIntoDocument(
1605+
<ProviderMock store={store}>
1606+
<Container />
1607+
</ProviderMock>
1608+
)
1609+
1610+
store.dispatch({ type: 'test' })
1611+
expect(updatedCount).toBe(0)
1612+
expect(memoizedReturnCount).toBe(2)
1613+
})
1614+
15121615
})
15131616
})

0 commit comments

Comments
 (0)