1
1
import hoistStatics from 'hoist-non-react-statics'
2
2
import invariant from 'invariant'
3
3
import { Component , createElement } from 'react'
4
- import { polyfill } from 'react-lifecycles-compat'
5
4
6
5
import Subscription from '../utils/Subscription'
7
6
import { storeShape , subscriptionShape } from '../utils/PropTypes'
8
7
9
8
let hotReloadingVersion = 0
9
+ const dummyState = { }
10
10
function noop ( ) { }
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 ,
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
20
21
}
21
- }
22
- return {
23
- shouldComponentUpdate : false ,
24
- }
25
- } catch ( error ) {
26
- return {
27
- shouldComponentUpdate : true ,
28
- error,
22
+ } catch ( error ) {
23
+ selector . shouldComponentUpdate = true
24
+ selector . error = error
29
25
}
30
26
}
31
27
}
28
+
29
+ return selector
32
30
}
33
31
34
32
export default function connectAdvanced (
@@ -88,10 +86,6 @@ export default function connectAdvanced(
88
86
[ subscriptionKey ] : subscriptionShape ,
89
87
}
90
88
91
- function getDerivedStateFromProps ( nextProps , prevState ) {
92
- return prevState . updater ( nextProps , prevState )
93
- }
94
-
95
89
return function wrapWithConnect ( WrappedComponent ) {
96
90
invariant (
97
91
typeof WrappedComponent == 'function' ,
@@ -118,11 +112,15 @@ export default function connectAdvanced(
118
112
WrappedComponent
119
113
}
120
114
115
+ // TODO Actually fix our use of componentWillReceiveProps
116
+ /* eslint-disable react/no-deprecated */
117
+
121
118
class Connect extends Component {
122
119
constructor ( props , context ) {
123
120
super ( props , context )
124
121
125
122
this . version = version
123
+ this . state = { }
126
124
this . renderCount = 0
127
125
this . store = props [ storeKey ] || context [ storeKey ]
128
126
this . propsMode = Boolean ( props [ storeKey ] )
@@ -134,9 +132,7 @@ export default function connectAdvanced(
134
132
`or explicitly pass "${ storeKey } " as a prop to "${ displayName } ".`
135
133
)
136
134
137
- this . state = {
138
- updater : this . createUpdater ( )
139
- }
135
+ this . initSelector ( )
140
136
this . initSubscription ( )
141
137
}
142
138
@@ -159,19 +155,25 @@ export default function connectAdvanced(
159
155
// dispatching an action in its componentWillMount, we have to re-run the select and maybe
160
156
// re-render.
161
157
this . subscription . trySubscribe ( )
162
- this . runUpdater ( )
158
+ this . selector . run ( this . props )
159
+ if ( this . selector . shouldComponentUpdate ) this . forceUpdate ( )
160
+ }
161
+
162
+ componentWillReceiveProps ( nextProps ) {
163
+ this . selector . run ( nextProps )
163
164
}
164
165
165
- shouldComponentUpdate ( _ , nextState ) {
166
- return nextState . shouldComponentUpdate
166
+ shouldComponentUpdate ( ) {
167
+ return this . selector . shouldComponentUpdate
167
168
}
168
169
169
170
componentWillUnmount ( ) {
170
171
if ( this . subscription ) this . subscription . tryUnsubscribe ( )
171
172
this . subscription = null
172
173
this . notifyNestedSubs = noop
173
174
this . store = null
174
- this . isUnmounted = true
175
+ this . selector . run = noop
176
+ this . selector . shouldComponentUpdate = false
175
177
}
176
178
177
179
getWrappedInstance ( ) {
@@ -186,17 +188,10 @@ export default function connectAdvanced(
186
188
this . wrappedInstance = ref
187
189
}
188
190
189
- createUpdater ( ) {
191
+ initSelector ( ) {
190
192
const sourceSelector = selectorFactory ( this . store . dispatch , selectorFactoryOptions )
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 )
193
+ this . selector = makeSelectorStateful ( sourceSelector , this . store )
194
+ this . selector . run ( this . props )
200
195
}
201
196
202
197
initSubscription ( ) {
@@ -217,7 +212,24 @@ export default function connectAdvanced(
217
212
}
218
213
219
214
onStateChange ( ) {
220
- this . runUpdater ( this . notifyNestedSubs )
215
+ this . selector . run ( this . props )
216
+
217
+ if ( ! this . selector . shouldComponentUpdate ) {
218
+ this . notifyNestedSubs ( )
219
+ } else {
220
+ this . componentDidUpdate = this . notifyNestedSubsOnComponentDidUpdate
221
+ this . setState ( dummyState )
222
+ }
223
+ }
224
+
225
+ notifyNestedSubsOnComponentDidUpdate ( ) {
226
+ // `componentDidUpdate` is conditionally implemented when `onStateChange` determines it
227
+ // needs to notify nested subs. Once called, it unimplements itself until further state
228
+ // changes occur. Doing it this way vs having a permanent `componentDidUpdate` that does
229
+ // a boolean check every time avoids an extra method call most of the time, resulting
230
+ // in some perf boost.
231
+ this . componentDidUpdate = undefined
232
+ this . notifyNestedSubs ( )
221
233
}
222
234
223
235
isSubscribed ( ) {
@@ -238,26 +250,31 @@ export default function connectAdvanced(
238
250
}
239
251
240
252
render ( ) {
241
- if ( this . state . error ) {
242
- throw this . state . error
253
+ const selector = this . selector
254
+ selector . shouldComponentUpdate = false
255
+
256
+ if ( selector . error ) {
257
+ throw selector . error
243
258
} else {
244
- return createElement ( WrappedComponent , this . addExtraProps ( this . state . props ) )
259
+ return createElement ( WrappedComponent , this . addExtraProps ( selector . props ) )
245
260
}
246
261
}
247
262
}
248
263
264
+ /* eslint-enable react/no-deprecated */
265
+
249
266
Connect . WrappedComponent = WrappedComponent
250
267
Connect . displayName = displayName
251
268
Connect . childContextTypes = childContextTypes
252
269
Connect . contextTypes = contextTypes
253
270
Connect . propTypes = contextTypes
254
- Connect . getDerivedStateFromProps = getDerivedStateFromProps
255
271
256
272
if ( process . env . NODE_ENV !== 'production' ) {
257
- Connect . prototype . componentDidUpdate = function componentDidUpdate ( ) {
273
+ Connect . prototype . componentWillUpdate = function componentWillUpdate ( ) {
258
274
// We are hot reloading!
259
275
if ( this . version !== version ) {
260
276
this . version = version
277
+ this . initSelector ( )
261
278
262
279
// If any connected descendants don't hot reload (and resubscribe in the process), their
263
280
// listeners will be lost when we unsubscribe. Unfortunately, by copying over all
@@ -275,16 +292,10 @@ export default function connectAdvanced(
275
292
this . subscription . trySubscribe ( )
276
293
oldListeners . forEach ( listener => this . subscription . listeners . subscribe ( listener ) )
277
294
}
278
-
279
- const updater = this . createUpdater ( )
280
- this . setState ( { updater} )
281
- this . runUpdater ( )
282
295
}
283
296
}
284
297
}
285
298
286
- polyfill ( Connect )
287
-
288
299
return hoistStatics ( Connect , WrappedComponent )
289
300
}
290
301
}
0 commit comments