Skip to content

Commit a7ba298

Browse files
committed
Stop using mutation
1 parent 7344f3d commit a7ba298

File tree

2 files changed

+44
-62
lines changed

2 files changed

+44
-62
lines changed

src/components/connectAdvanced.js

+40-58
Original file line numberDiff line numberDiff line change
@@ -7,31 +7,28 @@ import Subscription from '../utils/Subscription'
77
import { storeShape, subscriptionShape } from '../utils/PropTypes'
88

99
let hotReloadingVersion = 0
10-
const dummyState = {}
1110
function noop() {}
12-
function makeSelectorStateful(sourceSelector, store) {
13-
// wrap the selector in an object that tracks its results between runs.
14-
const selector = {
15-
run: function runComponentSelector(props) {
16-
try {
17-
const nextProps = sourceSelector(store.getState(), props)
18-
if (nextProps !== selector.props || selector.error) {
19-
selector.shouldComponentUpdate = true
20-
selector.props = nextProps
21-
selector.error = null
11+
function makeUpdater(sourceSelector, store) {
12+
return function updater(props, prevState) {
13+
try {
14+
const nextProps = sourceSelector(store.getState(), props)
15+
if (nextProps !== prevState.props || prevState.error) {
16+
return {
17+
shouldComponentUpdate: true,
18+
props: nextProps,
19+
error: null,
2220
}
23-
} catch (error) {
24-
selector.shouldComponentUpdate = true
25-
selector.error = error
2621
}
27-
},
28-
clean: function cleanComponentSelector() {
29-
selector.run = noop
30-
selector.shouldComponentUpdate = false
22+
return {
23+
shouldComponentUpdate: false,
24+
}
25+
} catch (error) {
26+
return {
27+
shouldComponentUpdate: true,
28+
error,
29+
}
3130
}
3231
}
33-
34-
return selector
3532
}
3633

3734
export default function connectAdvanced(
@@ -92,8 +89,7 @@ export default function connectAdvanced(
9289
}
9390

9491
function getDerivedStateFromProps(nextProps, prevState) {
95-
prevState.selector.run(nextProps)
96-
return null
92+
return prevState.updater(nextProps, prevState)
9793
}
9894

9995
return function wrapWithConnect(WrappedComponent) {
@@ -139,7 +135,7 @@ export default function connectAdvanced(
139135
)
140136

141137
this.state = {
142-
selector: this.createSelector()
138+
updater: this.createUpdater()
143139
}
144140
this.initSubscription()
145141
}
@@ -163,20 +159,19 @@ export default function connectAdvanced(
163159
// dispatching an action in its componentWillMount, we have to re-run the select and maybe
164160
// re-render.
165161
this.subscription.trySubscribe()
166-
this.state.selector.run(this.props)
167-
if (this.state.selector.shouldComponentUpdate) this.forceUpdate()
162+
this.runUpdater()
168163
}
169164

170-
shouldComponentUpdate(nextProps, nextState) {
171-
return nextState.selector.shouldComponentUpdate
165+
shouldComponentUpdate(_, nextState) {
166+
return nextState.shouldComponentUpdate
172167
}
173168

174169
componentWillUnmount() {
175170
if (this.subscription) this.subscription.tryUnsubscribe()
176171
this.subscription = null
177172
this.notifyNestedSubs = noop
178173
this.store = null
179-
this.state.selector.clean()
174+
this.isUnmounted = true
180175
}
181176

182177
getWrappedInstance() {
@@ -191,9 +186,17 @@ export default function connectAdvanced(
191186
this.wrappedInstance = ref
192187
}
193188

194-
createSelector() {
189+
createUpdater() {
195190
const sourceSelector = selectorFactory(this.store.dispatch, selectorFactoryOptions)
196-
return makeSelectorStateful(sourceSelector, this.store)
191+
return makeUpdater(sourceSelector, this.store)
192+
}
193+
194+
runUpdater(callback = noop) {
195+
if (this.isUnmounted) {
196+
return
197+
}
198+
199+
this.setState(prevState => prevState.updater(this.props, prevState), callback)
197200
}
198201

199202
initSubscription() {
@@ -214,24 +217,7 @@ export default function connectAdvanced(
214217
}
215218

216219
onStateChange() {
217-
this.state.selector.run(this.props)
218-
219-
if (!this.state.selector.shouldComponentUpdate) {
220-
this.notifyNestedSubs()
221-
} else {
222-
this.componentDidUpdate = this.notifyNestedSubsOnComponentDidUpdate
223-
this.setState(dummyState)
224-
}
225-
}
226-
227-
notifyNestedSubsOnComponentDidUpdate() {
228-
// `componentDidUpdate` is conditionally implemented when `onStateChange` determines it
229-
// needs to notify nested subs. Once called, it unimplements itself until further state
230-
// changes occur. Doing it this way vs having a permanent `componentDidUpdate` that does
231-
// a boolean check every time avoids an extra method call most of the time, resulting
232-
// in some perf boost.
233-
this.componentDidUpdate = undefined
234-
this.notifyNestedSubs()
220+
this.runUpdater(this.notifyNestedSubs)
235221
}
236222

237223
isSubscribed() {
@@ -252,14 +238,10 @@ export default function connectAdvanced(
252238
}
253239

254240
render() {
255-
const selector = this.state.selector
256-
257-
selector.shouldComponentUpdate = false
258-
259-
if (selector.error) {
260-
throw selector.error
241+
if (this.state.error) {
242+
throw this.state.error
261243
} else {
262-
return createElement(WrappedComponent, this.addExtraProps(selector.props))
244+
return createElement(WrappedComponent, this.addExtraProps(this.state.props))
263245
}
264246
}
265247
}
@@ -294,9 +276,9 @@ export default function connectAdvanced(
294276
oldListeners.forEach(listener => this.subscription.listeners.subscribe(listener))
295277
}
296278

297-
const selector = this.createSelector()
298-
selector.run(this.props)
299-
this.setState({selector})
279+
const updater = this.createUpdater()
280+
this.setState({updater})
281+
this.runUpdater()
300282
}
301283
}
302284
}

test/components/connect.spec.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -1045,7 +1045,7 @@ describe('React', () => {
10451045

10461046
spy.mockRestore()
10471047
document.body.removeChild(div)
1048-
expect(mapStateToPropsCalls).toBe(3)
1048+
expect(mapStateToPropsCalls).toBe(2)
10491049
expect(spy).toHaveBeenCalledTimes(0)
10501050
})
10511051

@@ -1856,17 +1856,17 @@ describe('React', () => {
18561856
store.dispatch({ type: 'APPEND', body: 'a' })
18571857
expect(mapStateCalls).toBe(2)
18581858
expect(renderCalls).toBe(1)
1859-
expect(spy).toHaveBeenCalledTimes(0)
1859+
expect(spy).toHaveBeenCalledTimes(1)
18601860

18611861
store.dispatch({ type: 'APPEND', body: 'a' })
18621862
expect(mapStateCalls).toBe(3)
18631863
expect(renderCalls).toBe(1)
1864-
expect(spy).toHaveBeenCalledTimes(0)
1864+
expect(spy).toHaveBeenCalledTimes(2)
18651865

18661866
store.dispatch({ type: 'APPEND', body: 'a' })
18671867
expect(mapStateCalls).toBe(4)
18681868
expect(renderCalls).toBe(2)
1869-
expect(spy).toHaveBeenCalledTimes(1)
1869+
expect(spy).toHaveBeenCalledTimes(3)
18701870

18711871
spy.mockRestore()
18721872
})

0 commit comments

Comments
 (0)