@@ -12,8 +12,8 @@ import Subscription from '../utils/Subscription'
12
12
13
13
import { ReactReduxContext } from './Context'
14
14
15
+ // Define some constant arrays just to avoid re-creating these
15
16
const EMPTY_ARRAY = [ ]
16
-
17
17
const NO_SUBSCRIPTION_ARRAY = [ null , null ]
18
18
19
19
const stringifyComponent = Comp => {
@@ -85,7 +85,7 @@ export default function connectAdvanced(
85
85
) {
86
86
invariant (
87
87
renderCountProp === undefined ,
88
- `renderCountProp is removed. render counting is built into the latest React dev tools profiling extension`
88
+ `renderCountProp is removed. render counting is built into the latest React Dev Tools profiling extension`
89
89
)
90
90
91
91
invariant (
@@ -141,24 +141,35 @@ export default function connectAdvanced(
141
141
return selectorFactory ( store . dispatch , selectorFactoryOptions )
142
142
}
143
143
144
- const usePureOnlyMemo = pure ? useMemo : x => x ( )
144
+ // If we aren't running in "pure" mode, we don't want to memoize values.
145
+ // To avoid conditionally calling hooks, we fall back to a tiny wrapper
146
+ // that just executes the given callback immediately.
147
+ const usePureOnlyMemo = pure ? useMemo : callback => callback ( )
145
148
146
149
function ConnectFunction ( props ) {
147
150
const [ propsContext , forwardedRef , wrapperProps ] = useMemo ( ( ) => {
151
+ // Distinguish between actual "data" props that were passed to the wrapper component,
152
+ // and values needed to control behavior (forwarded refs, alternate context instances).
153
+ // To maintain the wrapperProps object reference, memoize this destructuring.
148
154
const { context, forwardedRef, ...wrapperProps } = props
149
155
return [ context , forwardedRef , wrapperProps ]
150
156
} , [ props ] )
151
157
152
158
const ContextToUse = useMemo ( ( ) => {
159
+ // Users may optionally pass in a custom context instance to use instead of our ReactReduxContext.
160
+ // Memoize the check that determines which context instance we should use.
153
161
return propsContext &&
154
162
propsContext . Consumer &&
155
163
isContextConsumer ( < propsContext . Consumer /> )
156
164
? propsContext
157
165
: Context
158
166
} , [ propsContext , Context ] )
159
167
168
+ // Retrieve the store and ancestor subscription via context, if available
160
169
const contextValue = useContext ( ContextToUse )
161
170
171
+
172
+ // The store _must_ exist as either a prop or in context
162
173
invariant (
163
174
props . store || contextValue ,
164
175
`Could not find "store" in the context of ` +
@@ -168,8 +179,11 @@ export default function connectAdvanced(
168
179
)
169
180
170
181
const store = props . store || contextValue . store
182
+ const propsMode = Boolean ( props . store )
171
183
172
184
const childPropsSelector = useMemo ( ( ) => {
185
+ // The child props selector needs the store reference as an input.
186
+ // Re-create this selector whenever the store changes.
173
187
return createChildSelector ( store )
174
188
} , [ store ] )
175
189
@@ -178,44 +192,55 @@ export default function connectAdvanced(
178
192
179
193
// parentSub's source should match where store came from: props vs. context. A component
180
194
// connected to the store via props shouldn't use subscription from context, or vice versa.
181
- //const parentSub = //(this.propsMode ? this.props : this.context)[subscriptionKey]
182
195
const subscription = new Subscription ( store , contextValue . subscription )
183
196
184
197
// `notifyNestedSubs` is duplicated to handle the case where the component is unmounted in
185
- // the middle of the notification loop, where `this.subscription` will then be null. An
186
- // extra null check every change can be avoided by copying the method onto `this` and then
187
- // replacing it with a no-op on unmount. This can probably be avoided if Subscription's
188
- // listeners logic is changed to not call listeners that have been unsubscribed in the
189
- // middle of the notification loop.
198
+ // the middle of the notification loop, where `subscription` will then be null. This can
199
+ // probably be avoided if Subscription's listeners logic is changed to not call listeners
200
+ // that have been unsubscribed in the middle of the notification loop.
190
201
const notifyNestedSubs = subscription . notifyNestedSubs . bind (
191
202
subscription
192
203
)
193
204
194
205
return [ subscription , notifyNestedSubs ]
195
206
} , [ store , contextValue . subscription ] )
196
207
208
+ // Determine what {store, subscription} value should be put into nested context, if necessary
197
209
const overriddenContextValue = useMemo ( ( ) => {
210
+
211
+ // Otherwise, put this component's subscription instance into context, so that
212
+ // connected descendants won't update until after this component is done
198
213
return {
199
214
...contextValue ,
200
215
subscription
201
216
}
202
217
} , [ contextValue , subscription ] )
203
218
204
- const [ [ previousStateUpdateResult ] , dispatch ] = useReducer (
219
+ // We need to force this wrapper component to re-render whenever a Redux store update
220
+ // causes a change to the calculated child component props (or we caught an error in mapState)
221
+ const [ [ previousStateUpdateResult ] , forceComponentUpdateDispatch ] = useReducer (
205
222
storeStateUpdatesReducer ,
206
223
EMPTY_ARRAY ,
207
224
initStateUpdates
208
225
)
209
226
227
+ // Propagate any mapState/mapDispatch errors upwards
210
228
if ( previousStateUpdateResult && previousStateUpdateResult . error ) {
211
229
throw previousStateUpdateResult . error
212
230
}
213
231
232
+ // Set up refs to coordinate values between the subscription effect and the render logic
214
233
const lastChildProps = useRef ( )
215
234
const lastWrapperProps = useRef ( wrapperProps )
216
235
const childPropsFromStoreUpdate = useRef ( )
217
236
218
237
const actualChildProps = usePureOnlyMemo ( ( ) => {
238
+ // Tricky logic here:
239
+ // - This render may have been triggered by a Redux store update that produced new child props
240
+ // - However, we may have gotten new wrapper props after that
241
+ // If we have new child props, and the same wrapper props, we know we should use the new child props as-is.
242
+ // But, if we have new wrapper props, those might change the child props, so we have to recalculate things.
243
+ // So, we'll use the child props from store update only if the wrapper props are the same as last time.
219
244
if (
220
245
childPropsFromStoreUpdate . current &&
221
246
wrapperProps === lastWrapperProps . current
@@ -224,24 +249,33 @@ export default function connectAdvanced(
224
249
}
225
250
226
251
// TODO We're reading the store directly in render() here. Bad idea?
252
+ // This will likely cause Bad Things (TM) to happen in Concurrent Mode.
253
+ // Note that we do this because on renders _not_ caused by store updates, we need the latest store state
254
+ // to determine what the child props should be.
227
255
return childPropsSelector ( store . getState ( ) , wrapperProps )
228
256
} , [ store , previousStateUpdateResult , wrapperProps ] )
229
257
258
+ // Every time we do re-render:
230
259
useEffect ( ( ) => {
260
+ // We want to capture the wrapper props and child props we used for later comparisons
231
261
lastWrapperProps . current = wrapperProps
232
262
lastChildProps . current = actualChildProps
233
263
264
+ // If the render was from a store update, clear out that reference and cascade the subscriber update
234
265
if ( childPropsFromStoreUpdate . current ) {
235
266
childPropsFromStoreUpdate . current = null
236
267
notifyNestedSubs ( )
237
268
}
238
269
} )
239
270
271
+ // Our re-subscribe logic only runs when the store/subscription setup changes
240
272
useEffect ( ( ) => {
273
+ // If we're not subscribed to the store, nothing to do here
241
274
if ( ! shouldHandleStateChanges ) return
242
275
243
276
let didUnsubscribe = false
244
277
278
+ // We'll run this callback every time a store subscription update propagates to this component
245
279
const checkForUpdates = ( ) => {
246
280
if ( didUnsubscribe ) {
247
281
// Don't run stale listeners.
@@ -253,6 +287,8 @@ export default function connectAdvanced(
253
287
254
288
let newChildProps , error
255
289
try {
290
+ // Actually run the selector with the most recent store state and wrapper props
291
+ // to determine what the child props should be
256
292
newChildProps = childPropsSelector (
257
293
latestStoreState ,
258
294
lastWrapperProps . current
@@ -261,16 +297,23 @@ export default function connectAdvanced(
261
297
error = e
262
298
}
263
299
300
+ // If the child props haven't changed, nothing to do here - cascade the subscription update
264
301
if ( newChildProps === lastChildProps . current ) {
265
302
notifyNestedSubs ( )
266
303
} else {
267
- dispatch ( {
304
+ // If the child props _did_ change (or we caught an error), this wrapper component needs to re-render
305
+ forceComponentUpdateDispatch ( {
268
306
type : 'STORE_UPDATED' ,
269
307
payload : {
270
308
latestStoreState,
271
309
error
272
310
}
273
311
} )
312
+
313
+ // Save references to the new child props. Note that we track the "child props from store update"
314
+ // as a ref instead of a useState/useReducer because we need a way to determine if that value has
315
+ // been processed. If this went into useState/useReducer, we couldn't clear out the value without
316
+ // forcing another re-render, which we don't want.
274
317
lastChildProps . current = newChildProps
275
318
childPropsFromStoreUpdate . current = newChildProps
276
319
}
@@ -292,12 +335,20 @@ export default function connectAdvanced(
292
335
return unsubscribeWrapper
293
336
} , [ store , subscription , childPropsSelector ] )
294
337
338
+ // Now that all that's done, we can finally try to actually render the child component.
339
+ // We memoize the elements for the rendered child component as an optimization.
340
+ // If React sees the exact same element reference as last time, it bails out of re-rendering
341
+ // that child, same as if it was wrapped in React.memo() or returned false from shouldComponentUpdate.
295
342
const renderedChild = useMemo ( ( ) => {
343
+ // Render the actual child component
296
344
const renderedWrappedComponent = (
297
345
< WrappedComponent { ...actualChildProps } ref = { forwardedRef } />
298
346
)
299
347
300
348
if ( shouldHandleStateChanges ) {
349
+ // If this component is subscribed to store updates, we need to pass its own
350
+ // subscription instance down to our descendants. That means rendering the same
351
+ // Context instance, and putting a different value into the context.
301
352
return (
302
353
< ContextToUse . Provider value = { overriddenContextValue } >
303
354
{ renderedWrappedComponent }
@@ -317,6 +368,7 @@ export default function connectAdvanced(
317
368
return renderedChild
318
369
}
319
370
371
+ // If we're in "pure" mode, ensure our wrapper component only re-renders when incoming props have changed.
320
372
const Connect = pure ? React . memo ( ConnectFunction ) : ConnectFunction
321
373
322
374
Connect . WrappedComponent = WrappedComponent
0 commit comments