@@ -12,8 +12,8 @@ import Subscription from '../utils/Subscription'
1212
1313import { ReactReduxContext } from './Context'
1414
15+ // Define some constant arrays just to avoid re-creating these
1516const EMPTY_ARRAY = [ ]
16-
1717const NO_SUBSCRIPTION_ARRAY = [ null , null ]
1818
1919const stringifyComponent = Comp => {
@@ -85,7 +85,7 @@ export default function connectAdvanced(
8585) {
8686 invariant (
8787 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`
8989 )
9090
9191 invariant (
@@ -141,24 +141,35 @@ export default function connectAdvanced(
141141 return selectorFactory ( store . dispatch , selectorFactoryOptions )
142142 }
143143
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 ( )
145148
146149 function ConnectFunction ( props ) {
147150 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.
148154 const { context, forwardedRef, ...wrapperProps } = props
149155 return [ context , forwardedRef , wrapperProps ]
150156 } , [ props ] )
151157
152158 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.
153161 return propsContext &&
154162 propsContext . Consumer &&
155163 isContextConsumer ( < propsContext . Consumer /> )
156164 ? propsContext
157165 : Context
158166 } , [ propsContext , Context ] )
159167
168+ // Retrieve the store and ancestor subscription via context, if available
160169 const contextValue = useContext ( ContextToUse )
161170
171+
172+ // The store _must_ exist as either a prop or in context
162173 invariant (
163174 props . store || contextValue ,
164175 `Could not find "store" in the context of ` +
@@ -168,8 +179,11 @@ export default function connectAdvanced(
168179 )
169180
170181 const store = props . store || contextValue . store
182+ const propsMode = Boolean ( props . store )
171183
172184 const childPropsSelector = useMemo ( ( ) => {
185+ // The child props selector needs the store reference as an input.
186+ // Re-create this selector whenever the store changes.
173187 return createChildSelector ( store )
174188 } , [ store ] )
175189
@@ -178,44 +192,55 @@ export default function connectAdvanced(
178192
179193 // parentSub's source should match where store came from: props vs. context. A component
180194 // 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]
182195 const subscription = new Subscription ( store , contextValue . subscription )
183196
184197 // `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.
190201 const notifyNestedSubs = subscription . notifyNestedSubs . bind (
191202 subscription
192203 )
193204
194205 return [ subscription , notifyNestedSubs ]
195206 } , [ store , contextValue . subscription ] )
196207
208+ // Determine what {store, subscription} value should be put into nested context, if necessary
197209 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
198213 return {
199214 ...contextValue ,
200215 subscription
201216 }
202217 } , [ contextValue , subscription ] )
203218
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 (
205222 storeStateUpdatesReducer ,
206223 EMPTY_ARRAY ,
207224 initStateUpdates
208225 )
209226
227+ // Propagate any mapState/mapDispatch errors upwards
210228 if ( previousStateUpdateResult && previousStateUpdateResult . error ) {
211229 throw previousStateUpdateResult . error
212230 }
213231
232+ // Set up refs to coordinate values between the subscription effect and the render logic
214233 const lastChildProps = useRef ( )
215234 const lastWrapperProps = useRef ( wrapperProps )
216235 const childPropsFromStoreUpdate = useRef ( )
217236
218237 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.
219244 if (
220245 childPropsFromStoreUpdate . current &&
221246 wrapperProps === lastWrapperProps . current
@@ -224,24 +249,33 @@ export default function connectAdvanced(
224249 }
225250
226251 // 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.
227255 return childPropsSelector ( store . getState ( ) , wrapperProps )
228256 } , [ store , previousStateUpdateResult , wrapperProps ] )
229257
258+ // Every time we do re-render:
230259 useEffect ( ( ) => {
260+ // We want to capture the wrapper props and child props we used for later comparisons
231261 lastWrapperProps . current = wrapperProps
232262 lastChildProps . current = actualChildProps
233263
264+ // If the render was from a store update, clear out that reference and cascade the subscriber update
234265 if ( childPropsFromStoreUpdate . current ) {
235266 childPropsFromStoreUpdate . current = null
236267 notifyNestedSubs ( )
237268 }
238269 } )
239270
271+ // Our re-subscribe logic only runs when the store/subscription setup changes
240272 useEffect ( ( ) => {
273+ // If we're not subscribed to the store, nothing to do here
241274 if ( ! shouldHandleStateChanges ) return
242275
243276 let didUnsubscribe = false
244277
278+ // We'll run this callback every time a store subscription update propagates to this component
245279 const checkForUpdates = ( ) => {
246280 if ( didUnsubscribe ) {
247281 // Don't run stale listeners.
@@ -253,6 +287,8 @@ export default function connectAdvanced(
253287
254288 let newChildProps , error
255289 try {
290+ // Actually run the selector with the most recent store state and wrapper props
291+ // to determine what the child props should be
256292 newChildProps = childPropsSelector (
257293 latestStoreState ,
258294 lastWrapperProps . current
@@ -261,16 +297,23 @@ export default function connectAdvanced(
261297 error = e
262298 }
263299
300+ // If the child props haven't changed, nothing to do here - cascade the subscription update
264301 if ( newChildProps === lastChildProps . current ) {
265302 notifyNestedSubs ( )
266303 } else {
267- dispatch ( {
304+ // If the child props _did_ change (or we caught an error), this wrapper component needs to re-render
305+ forceComponentUpdateDispatch ( {
268306 type : 'STORE_UPDATED' ,
269307 payload : {
270308 latestStoreState,
271309 error
272310 }
273311 } )
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.
274317 lastChildProps . current = newChildProps
275318 childPropsFromStoreUpdate . current = newChildProps
276319 }
@@ -292,12 +335,20 @@ export default function connectAdvanced(
292335 return unsubscribeWrapper
293336 } , [ store , subscription , childPropsSelector ] )
294337
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.
295342 const renderedChild = useMemo ( ( ) => {
343+ // Render the actual child component
296344 const renderedWrappedComponent = (
297345 < WrappedComponent { ...actualChildProps } ref = { forwardedRef } />
298346 )
299347
300348 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.
301352 return (
302353 < ContextToUse . Provider value = { overriddenContextValue } >
303354 { renderedWrappedComponent }
@@ -317,6 +368,7 @@ export default function connectAdvanced(
317368 return renderedChild
318369 }
319370
371+ // If we're in "pure" mode, ensure our wrapper component only re-renders when incoming props have changed.
320372 const Connect = pure ? React . memo ( ConnectFunction ) : ConnectFunction
321373
322374 Connect . WrappedComponent = WrappedComponent
0 commit comments