Skip to content

Commit 01ce9bd

Browse files
committed
Add comments to connectAdvanced. Many much comments!
1 parent 8755d5a commit 01ce9bd

File tree

1 file changed

+63
-11
lines changed

1 file changed

+63
-11
lines changed

src/components/connectAdvanced.js

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ import Subscription from '../utils/Subscription'
1212

1313
import { ReactReduxContext } from './Context'
1414

15+
// Define some constant arrays just to avoid re-creating these
1516
const EMPTY_ARRAY = []
16-
1717
const NO_SUBSCRIPTION_ARRAY = [null, null]
1818

1919
const 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

Comments
 (0)