Skip to content

Commit db78da8

Browse files
committed
Merge pull request #225 from rackt/rewrite
Move the side effects out of shouldComponentUpdate()
2 parents 7eaf5dc + 8557615 commit db78da8

File tree

2 files changed

+141
-66
lines changed

2 files changed

+141
-66
lines changed

src/components/connect.js

+102-65
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { Component } from 'react'
1+
import React, { Component, createElement } from 'react'
22
import storeShape from '../utils/storeShape'
33
import shallowEqual from '../utils/shallowEqual'
44
import isPlainObject from '../utils/isPlainObject'
@@ -28,16 +28,16 @@ export default function connect(mapStateToProps, mapDispatchToProps, mergeProps,
2828
wrapActionCreators(mapDispatchToProps) :
2929
mapDispatchToProps || defaultMapDispatchToProps
3030
const finalMergeProps = mergeProps || defaultMergeProps
31-
const shouldUpdateStateProps = finalMapStateToProps.length !== 1
32-
const shouldUpdateDispatchProps = finalMapDispatchToProps.length !== 1
31+
const doStatePropsDependOnOwnProps = finalMapStateToProps.length !== 1
32+
const doDispatchPropsDependOnOwnProps = finalMapDispatchToProps.length !== 1
3333
const { pure = true, withRef = false } = options
3434

3535
// Helps track hot reloading.
3636
const version = nextVersion++
3737

3838
function computeStateProps(store, props) {
3939
const state = store.getState()
40-
const stateProps = shouldUpdateStateProps ?
40+
const stateProps = doStatePropsDependOnOwnProps ?
4141
finalMapStateToProps(state, props) :
4242
finalMapStateToProps(state)
4343

@@ -51,7 +51,7 @@ export default function connect(mapStateToProps, mapDispatchToProps, mergeProps,
5151

5252
function computeDispatchProps(store, props) {
5353
const { dispatch } = store
54-
const dispatchProps = shouldUpdateDispatchProps ?
54+
const dispatchProps = doDispatchPropsDependOnOwnProps ?
5555
finalMapDispatchToProps(dispatch, props) :
5656
finalMapDispatchToProps(dispatch)
5757

@@ -63,7 +63,7 @@ export default function connect(mapStateToProps, mapDispatchToProps, mergeProps,
6363
return dispatchProps
6464
}
6565

66-
function computeNextState(stateProps, dispatchProps, parentProps) {
66+
function computeMergedProps(stateProps, dispatchProps, parentProps) {
6767
const mergedProps = finalMergeProps(stateProps, dispatchProps, parentProps)
6868
invariant(
6969
isPlainObject(mergedProps),
@@ -75,33 +75,8 @@ export default function connect(mapStateToProps, mapDispatchToProps, mergeProps,
7575

7676
return function wrapWithConnect(WrappedComponent) {
7777
class Connect extends Component {
78-
shouldComponentUpdate(nextProps, nextState) {
79-
if (!pure) {
80-
this.updateStateProps(nextProps)
81-
this.updateDispatchProps(nextProps)
82-
this.updateState(nextProps)
83-
return true
84-
}
85-
86-
const storeChanged = nextState.storeState !== this.state.storeState
87-
const propsChanged = !shallowEqual(nextProps, this.props)
88-
let mapStateProducedChange = false
89-
let dispatchPropsChanged = false
90-
91-
if (storeChanged || (propsChanged && shouldUpdateStateProps)) {
92-
mapStateProducedChange = this.updateStateProps(nextProps)
93-
}
94-
95-
if (propsChanged && shouldUpdateDispatchProps) {
96-
dispatchPropsChanged = this.updateDispatchProps(nextProps)
97-
}
98-
99-
if (propsChanged || mapStateProducedChange || dispatchPropsChanged) {
100-
this.updateState(nextProps)
101-
return true
102-
}
103-
104-
return false
78+
shouldComponentUpdate() {
79+
return !pure || this.haveOwnPropsChanged || this.hasStoreStateChanged
10580
}
10681

10782
constructor(props, context) {
@@ -116,42 +91,37 @@ export default function connect(mapStateToProps, mapDispatchToProps, mergeProps,
11691
`or explicitly pass "store" as a prop to "${this.constructor.displayName}".`
11792
)
11893

119-
this.stateProps = computeStateProps(this.store, props)
120-
this.dispatchProps = computeDispatchProps(this.store, props)
121-
this.state = { storeState: this.store.getState() }
122-
this.updateState()
94+
const storeState = this.store.getState()
95+
this.state = { storeState }
96+
this.clearCache()
12397
}
12498

125-
computeNextState(props = this.props) {
126-
return computeNextState(
127-
this.stateProps,
128-
this.dispatchProps,
129-
props
130-
)
131-
}
132-
133-
updateStateProps(props = this.props) {
134-
const nextStateProps = computeStateProps(this.store, props)
135-
if (shallowEqual(nextStateProps, this.stateProps)) {
99+
updateStatePropsIfNeeded() {
100+
const nextStateProps = computeStateProps(this.store, this.props)
101+
if (this.stateProps && shallowEqual(nextStateProps, this.stateProps)) {
136102
return false
137103
}
138104

139105
this.stateProps = nextStateProps
140106
return true
141107
}
142108

143-
updateDispatchProps(props = this.props) {
144-
const nextDispatchProps = computeDispatchProps(this.store, props)
145-
if (shallowEqual(nextDispatchProps, this.dispatchProps)) {
109+
updateDispatchPropsIfNeeded() {
110+
const nextDispatchProps = computeDispatchProps(this.store, this.props)
111+
if (this.dispatchProps && shallowEqual(nextDispatchProps, this.dispatchProps)) {
146112
return false
147113
}
148114

149115
this.dispatchProps = nextDispatchProps
150116
return true
151117
}
152118

153-
updateState(props = this.props) {
154-
this.nextState = this.computeNextState(props)
119+
updateMergedProps() {
120+
this.mergedProps = computeMergedProps(
121+
this.stateProps,
122+
this.dispatchProps,
123+
this.props
124+
)
155125
}
156126

157127
isSubscribed() {
@@ -176,18 +146,38 @@ export default function connect(mapStateToProps, mapDispatchToProps, mergeProps,
176146
this.trySubscribe()
177147
}
178148

149+
componentWillReceiveProps(nextProps) {
150+
if (!pure || !shallowEqual(nextProps, this.props)) {
151+
this.haveOwnPropsChanged = true
152+
}
153+
}
154+
179155
componentWillUnmount() {
180156
this.tryUnsubscribe()
157+
this.clearCache()
158+
}
159+
160+
clearCache() {
161+
this.dispatchProps = null
162+
this.stateProps = null
163+
this.mergedProps = null
164+
this.haveOwnPropsChanged = true
165+
this.hasStoreStateChanged = true
166+
this.renderedElement = null
181167
}
182168

183169
handleChange() {
184170
if (!this.unsubscribe) {
185171
return
186172
}
187173

188-
this.setState({
189-
storeState: this.store.getState()
190-
})
174+
const prevStoreState = this.state.storeState
175+
const storeState = this.store.getState()
176+
177+
if (!pure || prevStoreState !== storeState) {
178+
this.hasStoreStateChanged = true
179+
this.setState({ storeState })
180+
}
191181
}
192182

193183
getWrappedInstance() {
@@ -200,10 +190,61 @@ export default function connect(mapStateToProps, mapDispatchToProps, mergeProps,
200190
}
201191

202192
render() {
203-
const ref = withRef ? 'wrappedInstance' : null
204-
return (
205-
<WrappedComponent {...this.nextState} ref={ref} />
206-
)
193+
const {
194+
haveOwnPropsChanged,
195+
hasStoreStateChanged,
196+
renderedElement
197+
} = this
198+
199+
this.haveOwnPropsChanged = false
200+
this.hasStoreStateChanged = false
201+
202+
let shouldUpdateStateProps = true
203+
let shouldUpdateDispatchProps = true
204+
if (pure && renderedElement) {
205+
shouldUpdateStateProps = hasStoreStateChanged || (
206+
haveOwnPropsChanged && doStatePropsDependOnOwnProps
207+
)
208+
shouldUpdateDispatchProps =
209+
haveOwnPropsChanged && doDispatchPropsDependOnOwnProps
210+
}
211+
212+
let haveStatePropsChanged = false
213+
let haveDispatchPropsChanged = false
214+
if (shouldUpdateStateProps) {
215+
haveStatePropsChanged = this.updateStatePropsIfNeeded()
216+
}
217+
if (shouldUpdateDispatchProps) {
218+
haveDispatchPropsChanged = this.updateDispatchPropsIfNeeded()
219+
}
220+
221+
let haveMergedPropsChanged = true
222+
if (
223+
haveStatePropsChanged ||
224+
haveDispatchPropsChanged ||
225+
haveOwnPropsChanged
226+
) {
227+
this.updateMergedProps()
228+
} else {
229+
haveMergedPropsChanged = false
230+
}
231+
232+
if (!haveMergedPropsChanged && renderedElement) {
233+
return renderedElement
234+
}
235+
236+
if (withRef) {
237+
this.renderedElement = createElement(WrappedComponent, {
238+
...this.mergedProps,
239+
ref: 'wrappedInstance'
240+
})
241+
} else {
242+
this.renderedElement = createElement(WrappedComponent,
243+
this.mergedProps
244+
)
245+
}
246+
247+
return this.renderedElement
207248
}
208249
}
209250

@@ -224,12 +265,8 @@ export default function connect(mapStateToProps, mapDispatchToProps, mergeProps,
224265

225266
// We are hot reloading!
226267
this.version = version
227-
228-
// Update the state and bindings.
229268
this.trySubscribe()
230-
this.updateStateProps()
231-
this.updateDispatchProps()
232-
this.updateState()
269+
this.clearCache()
233270
}
234271
}
235272

test/components/connect.spec.js

+39-1
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,44 @@ describe('React', () => {
201201
expect(stub.props.pass).toEqual('through')
202202
})
203203

204+
it('should handle unexpected prop changes with forceUpdate()', () => {
205+
const store = createStore(() => ({}))
206+
207+
@connect(state => state)
208+
class ConnectContainer extends Component {
209+
render() {
210+
return (
211+
<Passthrough {...this.props} pass={this.props.bar} />
212+
)
213+
}
214+
}
215+
216+
class Container extends Component {
217+
constructor() {
218+
super()
219+
this.bar = 'baz'
220+
}
221+
222+
componentDidMount() {
223+
this.bar = 'foo'
224+
this.forceUpdate()
225+
this.c.forceUpdate()
226+
}
227+
228+
render() {
229+
return (
230+
<ProviderMock store={store}>
231+
<ConnectContainer bar={this.bar} ref={c => this.c = c} />
232+
</ProviderMock>
233+
)
234+
}
235+
}
236+
237+
const container = TestUtils.renderIntoDocument(<Container />)
238+
const stub = TestUtils.findRenderedComponentWithType(container, Passthrough)
239+
expect(stub.props.bar).toEqual('foo')
240+
})
241+
204242
it('should remove undefined props', () => {
205243
const store = createStore(() => ({}))
206244
let props = { x: true }
@@ -323,7 +361,7 @@ describe('React', () => {
323361
return (
324362
<ProviderMock store={store}>
325363
<ConnectContainer bar={this.state.bar} />
326-
</ProviderMock>
364+
</ProviderMock>
327365
)
328366
}
329367
}

0 commit comments

Comments
 (0)