Skip to content

Commit 2a3c22e

Browse files
Hypnosphijosepot
authored andcommitted
Remove usages of async-unsafe lifecycle methods (reduxjs#919)
* Reformat to make VS Code less annoying * Failing test in <StrictMode> * Remove usages of async-unsafe lifecycle methods * Remove usage of UNSAFE_componentWillMount in test * Move `selector` to state in order to be able to use gDSFP * codeDIdCommit * Stop using mutation * Upgrade react-lifecycles-compat
1 parent 4cc74fd commit 2a3c22e

File tree

7 files changed

+120
-81
lines changed

7 files changed

+120
-81
lines changed

docs/api.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ It does not modify the component class passed to it; instead, it *returns* a new
377377

378378
* [`renderCountProp`] *(String)*: if defined, a property named this value will be added to the props passed to the wrapped component. Its value will be the number of times the component has been rendered, which can be useful for tracking down unnecessary re-renders. Default value: `undefined`
379379

380-
* [`shouldHandleStateChanges`] *(Boolean)*: controls whether the connector component subscribes to redux store state changes. If set to false, it will only re-render on `componentWillReceiveProps`. Default value: `true`
380+
* [`shouldHandleStateChanges`] *(Boolean)*: controls whether the connector component subscribes to redux store state changes. If set to false, it will only re-render when parent component re-renders. Default value: `true`
381381

382382
* [`storeKey`] *(String)*: the key of props/context to get the store. You probably only need this if you are in the inadvisable position of having multiple stores. Default value: `'store'`
383383

package-lock.json

+11
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@
4646
"hoist-non-react-statics": "^2.5.0",
4747
"invariant": "^2.2.4",
4848
"loose-envify": "^1.1.0",
49-
"prop-types": "^15.6.1"
49+
"prop-types": "^15.6.1",
50+
"react-lifecycles-compat": "^3.0.0"
5051
},
5152
"devDependencies": {
5253
"babel-cli": "^6.26.0",

src/components/Provider.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ export function createProvider(storeKey = 'store') {
3838
}
3939

4040
if (process.env.NODE_ENV !== 'production') {
41-
Provider.prototype.componentWillReceiveProps = function (nextProps) {
42-
if (this[storeKey] !== nextProps.store) {
41+
Provider.prototype.componentDidUpdate = function () {
42+
if (this[storeKey] !== this.props.store) {
4343
warnAboutReceivingStore()
4444
}
4545
}

src/components/connectAdvanced.js

+51-57
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,34 @@
11
import hoistStatics from 'hoist-non-react-statics'
22
import invariant from 'invariant'
33
import { Component, createElement } from 'react'
4+
import { polyfill } from 'react-lifecycles-compat'
45

56
import Subscription from '../utils/Subscription'
67
import { storeShape, subscriptionShape } from '../utils/PropTypes'
78

89
let hotReloadingVersion = 0
9-
const dummyState = {}
1010
function noop() {}
11-
function makeSelectorStateful(sourceSelector, store) {
12-
// wrap the selector in an object that tracks its results between runs.
13-
const selector = {
14-
run: function runComponentSelector(props) {
15-
try {
16-
const nextProps = sourceSelector(store.getState(), props)
17-
if (nextProps !== selector.props || selector.error) {
18-
selector.shouldComponentUpdate = true
19-
selector.props = nextProps
20-
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,
2120
}
22-
} catch (error) {
23-
selector.shouldComponentUpdate = true
24-
selector.error = error
21+
}
22+
return {
23+
shouldComponentUpdate: false,
24+
}
25+
} catch (error) {
26+
return {
27+
shouldComponentUpdate: true,
28+
error,
2529
}
2630
}
2731
}
28-
29-
return selector
3032
}
3133

3234
export default function connectAdvanced(
@@ -86,6 +88,10 @@ export default function connectAdvanced(
8688
[subscriptionKey]: subscriptionShape,
8789
}
8890

91+
function getDerivedStateFromProps(nextProps, prevState) {
92+
return prevState.updater(nextProps, prevState)
93+
}
94+
8995
return function wrapWithConnect(WrappedComponent) {
9096
invariant(
9197
typeof WrappedComponent == 'function',
@@ -117,7 +123,6 @@ export default function connectAdvanced(
117123
super(props, context)
118124

119125
this.version = version
120-
this.state = {}
121126
this.renderCount = 0
122127
this.store = props[storeKey] || context[storeKey]
123128
this.propsMode = Boolean(props[storeKey])
@@ -129,7 +134,9 @@ export default function connectAdvanced(
129134
`or explicitly pass "${storeKey}" as a prop to "${displayName}".`
130135
)
131136

132-
this.initSelector()
137+
this.state = {
138+
updater: this.createUpdater()
139+
}
133140
this.initSubscription()
134141
}
135142

@@ -152,25 +159,19 @@ export default function connectAdvanced(
152159
// dispatching an action in its componentWillMount, we have to re-run the select and maybe
153160
// re-render.
154161
this.subscription.trySubscribe()
155-
this.selector.run(this.props)
156-
if (this.selector.shouldComponentUpdate) this.forceUpdate()
157-
}
158-
159-
componentWillReceiveProps(nextProps) {
160-
this.selector.run(nextProps)
162+
this.runUpdater()
161163
}
162164

163-
shouldComponentUpdate() {
164-
return this.selector.shouldComponentUpdate
165+
shouldComponentUpdate(_, nextState) {
166+
return nextState.shouldComponentUpdate
165167
}
166168

167169
componentWillUnmount() {
168170
if (this.subscription) this.subscription.tryUnsubscribe()
169171
this.subscription = null
170172
this.notifyNestedSubs = noop
171173
this.store = null
172-
this.selector.run = noop
173-
this.selector.shouldComponentUpdate = false
174+
this.isUnmounted = true
174175
}
175176

176177
getWrappedInstance() {
@@ -185,10 +186,17 @@ export default function connectAdvanced(
185186
this.wrappedInstance = ref
186187
}
187188

188-
initSelector() {
189+
createUpdater() {
189190
const sourceSelector = selectorFactory(this.store.dispatch, selectorFactoryOptions)
190-
this.selector = makeSelectorStateful(sourceSelector, this.store)
191-
this.selector.run(this.props)
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)
192200
}
193201

194202
initSubscription() {
@@ -209,24 +217,7 @@ export default function connectAdvanced(
209217
}
210218

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

232223
isSubscribed() {
@@ -247,13 +238,10 @@ export default function connectAdvanced(
247238
}
248239

249240
render() {
250-
const selector = this.selector
251-
selector.shouldComponentUpdate = false
252-
253-
if (selector.error) {
254-
throw selector.error
241+
if (this.state.error) {
242+
throw this.state.error
255243
} else {
256-
return createElement(WrappedComponent, this.addExtraProps(selector.props))
244+
return createElement(WrappedComponent, this.addExtraProps(this.state.props))
257245
}
258246
}
259247
}
@@ -263,13 +251,13 @@ export default function connectAdvanced(
263251
Connect.childContextTypes = childContextTypes
264252
Connect.contextTypes = contextTypes
265253
Connect.propTypes = contextTypes
254+
Connect.getDerivedStateFromProps = getDerivedStateFromProps
266255

267256
if (process.env.NODE_ENV !== 'production') {
268-
Connect.prototype.componentWillUpdate = function componentWillUpdate() {
257+
Connect.prototype.componentDidUpdate = function componentDidUpdate() {
269258
// We are hot reloading!
270259
if (this.version !== version) {
271260
this.version = version
272-
this.initSelector()
273261

274262
// If any connected descendants don't hot reload (and resubscribe in the process), their
275263
// listeners will be lost when we unsubscribe. Unfortunately, by copying over all
@@ -287,10 +275,16 @@ export default function connectAdvanced(
287275
this.subscription.trySubscribe()
288276
oldListeners.forEach(listener => this.subscription.listeners.subscribe(listener))
289277
}
278+
279+
const updater = this.createUpdater()
280+
this.setState({updater})
281+
this.runUpdater()
290282
}
291283
}
292284
}
293285

286+
polyfill(Connect)
287+
294288
return hoistStatics(Connect, WrappedComponent)
295289
}
296290
}

test/components/Provider.spec.js

+15
Original file line numberDiff line numberDiff line change
@@ -220,4 +220,19 @@ describe('React', () => {
220220
store.dispatch({ type: 'APPEND', body: 'd' })
221221
expect(childMapStateInvokes).toBe(4)
222222
})
223+
224+
it('works in <StrictMode> without warnings', () => {
225+
const spy = jest.spyOn(console, 'error').mockImplementation(() => {})
226+
const store = createStore(() => ({}))
227+
228+
TestRenderer.create(
229+
<React.StrictMode>
230+
<Provider store={store}>
231+
<div />
232+
</Provider>
233+
</React.StrictMode>
234+
)
235+
236+
expect(spy).not.toHaveBeenCalled()
237+
})
223238
})

0 commit comments

Comments
 (0)