source: node_modules/react-redux/src/components/connect.tsx

Last change on this file was a762898, checked in by istevanoska <ilinastevanoska@…>, 5 months ago

Added visualizations

  • Property mode set to 100644
File size: 30.1 KB
RevLine 
[a762898]1/* eslint-disable valid-jsdoc, @typescript-eslint/no-unused-vars */
2import type { ComponentType } from 'react'
3import { React } from '../utils/react'
4import { isValidElementType, isContextConsumer } from '../utils/react-is'
5
6import type { Store } from 'redux'
7
8import type {
9 ConnectedComponent,
10 InferableComponentEnhancer,
11 InferableComponentEnhancerWithProps,
12 ResolveThunks,
13 DispatchProp,
14 ConnectPropsMaybeWithoutContext,
15} from '../types'
16
17import type {
18 MapStateToPropsParam,
19 MapDispatchToPropsParam,
20 MergeProps,
21 MapDispatchToPropsNonObject,
22 SelectorFactoryOptions,
23} from '../connect/selectorFactory'
24import defaultSelectorFactory from '../connect/selectorFactory'
25import { mapDispatchToPropsFactory } from '../connect/mapDispatchToProps'
26import { mapStateToPropsFactory } from '../connect/mapStateToProps'
27import { mergePropsFactory } from '../connect/mergeProps'
28
29import type { Subscription } from '../utils/Subscription'
30import { createSubscription } from '../utils/Subscription'
31import { useIsomorphicLayoutEffect } from '../utils/useIsomorphicLayoutEffect'
32import shallowEqual from '../utils/shallowEqual'
33import hoistStatics from '../utils/hoistStatics'
34import warning from '../utils/warning'
35
36import type {
37 ReactReduxContextValue,
38 ReactReduxContextInstance,
39} from './Context'
40import { ReactReduxContext } from './Context'
41
42// Define some constant arrays just to avoid re-creating these
43const EMPTY_ARRAY: [unknown, number] = [null, 0]
44const NO_SUBSCRIPTION_ARRAY = [null, null]
45
46// Attempts to stringify whatever not-really-a-component value we were given
47// for logging in an error message
48const stringifyComponent = (Comp: unknown) => {
49 try {
50 return JSON.stringify(Comp)
51 } catch (err) {
52 return String(Comp)
53 }
54}
55
56type EffectFunc = (...args: any[]) => void | ReturnType<React.EffectCallback>
57
58// This is "just" a `useLayoutEffect`, but with two modifications:
59// - we need to fall back to `useEffect` in SSR to avoid annoying warnings
60// - we extract this to a separate function to avoid closing over values
61// and causing memory leaks
62function useIsomorphicLayoutEffectWithArgs(
63 effectFunc: EffectFunc,
64 effectArgs: any[],
65 dependencies?: React.DependencyList,
66) {
67 useIsomorphicLayoutEffect(() => effectFunc(...effectArgs), dependencies)
68}
69
70// Effect callback, extracted: assign the latest props values to refs for later usage
71function captureWrapperProps(
72 lastWrapperProps: React.MutableRefObject<unknown>,
73 lastChildProps: React.MutableRefObject<unknown>,
74 renderIsScheduled: React.MutableRefObject<boolean>,
75 wrapperProps: unknown,
76 // actualChildProps: unknown,
77 childPropsFromStoreUpdate: React.MutableRefObject<unknown>,
78 notifyNestedSubs: () => void,
79) {
80 // We want to capture the wrapper props and child props we used for later comparisons
81 lastWrapperProps.current = wrapperProps
82 renderIsScheduled.current = false
83
84 // If the render was from a store update, clear out that reference and cascade the subscriber update
85 if (childPropsFromStoreUpdate.current) {
86 childPropsFromStoreUpdate.current = null
87 notifyNestedSubs()
88 }
89}
90
91// Effect callback, extracted: subscribe to the Redux store or nearest connected ancestor,
92// check for updates after dispatched actions, and trigger re-renders.
93function subscribeUpdates(
94 shouldHandleStateChanges: boolean,
95 store: Store,
96 subscription: Subscription,
97 childPropsSelector: (state: unknown, props: unknown) => unknown,
98 lastWrapperProps: React.MutableRefObject<unknown>,
99 lastChildProps: React.MutableRefObject<unknown>,
100 renderIsScheduled: React.MutableRefObject<boolean>,
101 isMounted: React.MutableRefObject<boolean>,
102 childPropsFromStoreUpdate: React.MutableRefObject<unknown>,
103 notifyNestedSubs: () => void,
104 // forceComponentUpdateDispatch: React.Dispatch<any>,
105 additionalSubscribeListener: () => void,
106) {
107 // If we're not subscribed to the store, nothing to do here
108 if (!shouldHandleStateChanges) return () => {}
109
110 // Capture values for checking if and when this component unmounts
111 let didUnsubscribe = false
112 let lastThrownError: Error | null = null
113
114 // We'll run this callback every time a store subscription update propagates to this component
115 const checkForUpdates = () => {
116 if (didUnsubscribe || !isMounted.current) {
117 // Don't run stale listeners.
118 // Redux doesn't guarantee unsubscriptions happen until next dispatch.
119 return
120 }
121
122 // TODO We're currently calling getState ourselves here, rather than letting `uSES` do it
123 const latestStoreState = store.getState()
124
125 let newChildProps, error
126 try {
127 // Actually run the selector with the most recent store state and wrapper props
128 // to determine what the child props should be
129 newChildProps = childPropsSelector(
130 latestStoreState,
131 lastWrapperProps.current,
132 )
133 } catch (e) {
134 error = e
135 lastThrownError = e as Error | null
136 }
137
138 if (!error) {
139 lastThrownError = null
140 }
141
142 // If the child props haven't changed, nothing to do here - cascade the subscription update
143 if (newChildProps === lastChildProps.current) {
144 if (!renderIsScheduled.current) {
145 notifyNestedSubs()
146 }
147 } else {
148 // Save references to the new child props. Note that we track the "child props from store update"
149 // as a ref instead of a useState/useReducer because we need a way to determine if that value has
150 // been processed. If this went into useState/useReducer, we couldn't clear out the value without
151 // forcing another re-render, which we don't want.
152 lastChildProps.current = newChildProps
153 childPropsFromStoreUpdate.current = newChildProps
154 renderIsScheduled.current = true
155
156 // TODO This is hacky and not how `uSES` is meant to be used
157 // Trigger the React `useSyncExternalStore` subscriber
158 additionalSubscribeListener()
159 }
160 }
161
162 // Actually subscribe to the nearest connected ancestor (or store)
163 subscription.onStateChange = checkForUpdates
164 subscription.trySubscribe()
165
166 // Pull data from the store after first render in case the store has
167 // changed since we began.
168 checkForUpdates()
169
170 const unsubscribeWrapper = () => {
171 didUnsubscribe = true
172 subscription.tryUnsubscribe()
173 subscription.onStateChange = null
174
175 if (lastThrownError) {
176 // It's possible that we caught an error due to a bad mapState function, but the
177 // parent re-rendered without this component and we're about to unmount.
178 // This shouldn't happen as long as we do top-down subscriptions correctly, but
179 // if we ever do those wrong, this throw will surface the error in our tests.
180 // In that case, throw the error from here so it doesn't get lost.
181 throw lastThrownError
182 }
183 }
184
185 return unsubscribeWrapper
186}
187
188// Reducer initial state creation for our update reducer
189const initStateUpdates = () => EMPTY_ARRAY
190
191export interface ConnectProps {
192 /** A custom Context instance that the component can use to access the store from an alternate Provider using that same Context instance */
193 context?: ReactReduxContextInstance
194 /** A Redux store instance to be used for subscriptions instead of the store from a Provider */
195 store?: Store
196}
197
198interface InternalConnectProps extends ConnectProps {
199 reactReduxForwardedRef?: React.ForwardedRef<unknown>
200}
201
202function strictEqual(a: unknown, b: unknown) {
203 return a === b
204}
205
206/**
207 * Infers the type of props that a connector will inject into a component.
208 */
209export type ConnectedProps<TConnector> =
210 TConnector extends InferableComponentEnhancerWithProps<
211 infer TInjectedProps,
212 any
213 >
214 ? unknown extends TInjectedProps
215 ? TConnector extends InferableComponentEnhancer<infer TInjectedProps>
216 ? TInjectedProps
217 : never
218 : TInjectedProps
219 : never
220
221export interface ConnectOptions<
222 State = unknown,
223 TStateProps = {},
224 TOwnProps = {},
225 TMergedProps = {},
226> {
227 forwardRef?: boolean
228 context?: typeof ReactReduxContext
229 areStatesEqual?: (
230 nextState: State,
231 prevState: State,
232 nextOwnProps: TOwnProps,
233 prevOwnProps: TOwnProps,
234 ) => boolean
235
236 areOwnPropsEqual?: (
237 nextOwnProps: TOwnProps,
238 prevOwnProps: TOwnProps,
239 ) => boolean
240
241 areStatePropsEqual?: (
242 nextStateProps: TStateProps,
243 prevStateProps: TStateProps,
244 ) => boolean
245 areMergedPropsEqual?: (
246 nextMergedProps: TMergedProps,
247 prevMergedProps: TMergedProps,
248 ) => boolean
249}
250
251/**
252 * Connects a React component to a Redux store.
253 *
254 * - Without arguments, just wraps the component, without changing the behavior / props
255 *
256 * - If 2 params are passed (3rd param, mergeProps, is skipped), default behavior
257 * is to override ownProps (as stated in the docs), so what remains is everything that's
258 * not a state or dispatch prop
259 *
260 * - When 3rd param is passed, we don't know if ownProps propagate and whether they
261 * should be valid component props, because it depends on mergeProps implementation.
262 * As such, it is the user's responsibility to extend ownProps interface from state or
263 * dispatch props or both when applicable
264 *
265 * @param mapStateToProps
266 * @param mapDispatchToProps
267 * @param mergeProps
268 * @param options
269 */
270export interface Connect<DefaultState = unknown> {
271 // tslint:disable:no-unnecessary-generics
272 (): InferableComponentEnhancer<DispatchProp>
273
274 /** mapState only */
275 <TStateProps = {}, no_dispatch = {}, TOwnProps = {}, State = DefaultState>(
276 mapStateToProps: MapStateToPropsParam<TStateProps, TOwnProps, State>,
277 ): InferableComponentEnhancerWithProps<TStateProps & DispatchProp, TOwnProps>
278
279 /** mapDispatch only (as a function) */
280 <no_state = {}, TDispatchProps = {}, TOwnProps = {}>(
281 mapStateToProps: null | undefined,
282 mapDispatchToProps: MapDispatchToPropsNonObject<TDispatchProps, TOwnProps>,
283 ): InferableComponentEnhancerWithProps<TDispatchProps, TOwnProps>
284
285 /** mapDispatch only (as an object) */
286 <no_state = {}, TDispatchProps = {}, TOwnProps = {}>(
287 mapStateToProps: null | undefined,
288 mapDispatchToProps: MapDispatchToPropsParam<TDispatchProps, TOwnProps>,
289 ): InferableComponentEnhancerWithProps<
290 ResolveThunks<TDispatchProps>,
291 TOwnProps
292 >
293
294 /** mapState and mapDispatch (as a function)*/
295 <TStateProps = {}, TDispatchProps = {}, TOwnProps = {}, State = DefaultState>(
296 mapStateToProps: MapStateToPropsParam<TStateProps, TOwnProps, State>,
297 mapDispatchToProps: MapDispatchToPropsNonObject<TDispatchProps, TOwnProps>,
298 ): InferableComponentEnhancerWithProps<
299 TStateProps & TDispatchProps,
300 TOwnProps
301 >
302
303 /** mapState and mapDispatch (nullish) */
304 <TStateProps = {}, TDispatchProps = {}, TOwnProps = {}, State = DefaultState>(
305 mapStateToProps: MapStateToPropsParam<TStateProps, TOwnProps, State>,
306 mapDispatchToProps: null | undefined,
307 ): InferableComponentEnhancerWithProps<TStateProps, TOwnProps>
308
309 /** mapState and mapDispatch (as an object) */
310 <TStateProps = {}, TDispatchProps = {}, TOwnProps = {}, State = DefaultState>(
311 mapStateToProps: MapStateToPropsParam<TStateProps, TOwnProps, State>,
312 mapDispatchToProps: MapDispatchToPropsParam<TDispatchProps, TOwnProps>,
313 ): InferableComponentEnhancerWithProps<
314 TStateProps & ResolveThunks<TDispatchProps>,
315 TOwnProps
316 >
317
318 /** mergeProps only */
319 <no_state = {}, no_dispatch = {}, TOwnProps = {}, TMergedProps = {}>(
320 mapStateToProps: null | undefined,
321 mapDispatchToProps: null | undefined,
322 mergeProps: MergeProps<undefined, DispatchProp, TOwnProps, TMergedProps>,
323 ): InferableComponentEnhancerWithProps<TMergedProps, TOwnProps>
324
325 /** mapState and mergeProps */
326 <
327 TStateProps = {},
328 no_dispatch = {},
329 TOwnProps = {},
330 TMergedProps = {},
331 State = DefaultState,
332 >(
333 mapStateToProps: MapStateToPropsParam<TStateProps, TOwnProps, State>,
334 mapDispatchToProps: null | undefined,
335 mergeProps: MergeProps<TStateProps, DispatchProp, TOwnProps, TMergedProps>,
336 ): InferableComponentEnhancerWithProps<TMergedProps, TOwnProps>
337
338 /** mapDispatch (as a object) and mergeProps */
339 <no_state = {}, TDispatchProps = {}, TOwnProps = {}, TMergedProps = {}>(
340 mapStateToProps: null | undefined,
341 mapDispatchToProps: MapDispatchToPropsParam<TDispatchProps, TOwnProps>,
342 mergeProps: MergeProps<undefined, TDispatchProps, TOwnProps, TMergedProps>,
343 ): InferableComponentEnhancerWithProps<TMergedProps, TOwnProps>
344
345 /** mapState and options */
346 <TStateProps = {}, no_dispatch = {}, TOwnProps = {}, State = DefaultState>(
347 mapStateToProps: MapStateToPropsParam<TStateProps, TOwnProps, State>,
348 mapDispatchToProps: null | undefined,
349 mergeProps: null | undefined,
350 options: ConnectOptions<State, TStateProps, TOwnProps>,
351 ): InferableComponentEnhancerWithProps<DispatchProp & TStateProps, TOwnProps>
352
353 /** mapDispatch (as a function) and options */
354 <TStateProps = {}, TDispatchProps = {}, TOwnProps = {}>(
355 mapStateToProps: null | undefined,
356 mapDispatchToProps: MapDispatchToPropsNonObject<TDispatchProps, TOwnProps>,
357 mergeProps: null | undefined,
358 options: ConnectOptions<{}, TStateProps, TOwnProps>,
359 ): InferableComponentEnhancerWithProps<TDispatchProps, TOwnProps>
360
361 /** mapDispatch (as an object) and options*/
362 <TStateProps = {}, TDispatchProps = {}, TOwnProps = {}>(
363 mapStateToProps: null | undefined,
364 mapDispatchToProps: MapDispatchToPropsParam<TDispatchProps, TOwnProps>,
365 mergeProps: null | undefined,
366 options: ConnectOptions<{}, TStateProps, TOwnProps>,
367 ): InferableComponentEnhancerWithProps<
368 ResolveThunks<TDispatchProps>,
369 TOwnProps
370 >
371
372 /** mapState, mapDispatch (as a function), and options */
373 <TStateProps = {}, TDispatchProps = {}, TOwnProps = {}, State = DefaultState>(
374 mapStateToProps: MapStateToPropsParam<TStateProps, TOwnProps, State>,
375 mapDispatchToProps: MapDispatchToPropsNonObject<TDispatchProps, TOwnProps>,
376 mergeProps: null | undefined,
377 options: ConnectOptions<State, TStateProps, TOwnProps>,
378 ): InferableComponentEnhancerWithProps<
379 TStateProps & TDispatchProps,
380 TOwnProps
381 >
382
383 /** mapState, mapDispatch (as an object), and options */
384 <TStateProps = {}, TDispatchProps = {}, TOwnProps = {}, State = DefaultState>(
385 mapStateToProps: MapStateToPropsParam<TStateProps, TOwnProps, State>,
386 mapDispatchToProps: MapDispatchToPropsParam<TDispatchProps, TOwnProps>,
387 mergeProps: null | undefined,
388 options: ConnectOptions<State, TStateProps, TOwnProps>,
389 ): InferableComponentEnhancerWithProps<
390 TStateProps & ResolveThunks<TDispatchProps>,
391 TOwnProps
392 >
393
394 /** mapState, mapDispatch, mergeProps, and options */
395 <
396 TStateProps = {},
397 TDispatchProps = {},
398 TOwnProps = {},
399 TMergedProps = {},
400 State = DefaultState,
401 >(
402 mapStateToProps: MapStateToPropsParam<TStateProps, TOwnProps, State>,
403 mapDispatchToProps: MapDispatchToPropsParam<TDispatchProps, TOwnProps>,
404 mergeProps: MergeProps<
405 TStateProps,
406 TDispatchProps,
407 TOwnProps,
408 TMergedProps
409 >,
410 options?: ConnectOptions<State, TStateProps, TOwnProps, TMergedProps>,
411 ): InferableComponentEnhancerWithProps<TMergedProps, TOwnProps>
412 // tslint:enable:no-unnecessary-generics
413}
414
415let hasWarnedAboutDeprecatedPureOption = false
416
417/**
418 * Connects a React component to a Redux store.
419 *
420 * - Without arguments, just wraps the component, without changing the behavior / props
421 *
422 * - If 2 params are passed (3rd param, mergeProps, is skipped), default behavior
423 * is to override ownProps (as stated in the docs), so what remains is everything that's
424 * not a state or dispatch prop
425 *
426 * - When 3rd param is passed, we don't know if ownProps propagate and whether they
427 * should be valid component props, because it depends on mergeProps implementation.
428 * As such, it is the user's responsibility to extend ownProps interface from state or
429 * dispatch props or both when applicable
430 *
431 * @param mapStateToProps A function that extracts values from state
432 * @param mapDispatchToProps Setup for dispatching actions
433 * @param mergeProps Optional callback to merge state and dispatch props together
434 * @param options Options for configuring the connection
435 *
436 */
437function connect<
438 TStateProps = {},
439 TDispatchProps = {},
440 TOwnProps = {},
441 TMergedProps = {},
442 State = unknown,
443>(
444 mapStateToProps?: MapStateToPropsParam<TStateProps, TOwnProps, State>,
445 mapDispatchToProps?: MapDispatchToPropsParam<TDispatchProps, TOwnProps>,
446 mergeProps?: MergeProps<TStateProps, TDispatchProps, TOwnProps, TMergedProps>,
447 {
448 // The `pure` option has been removed, so TS doesn't like us destructuring this to check its existence.
449 // @ts-ignore
450 pure,
451 areStatesEqual = strictEqual,
452 areOwnPropsEqual = shallowEqual,
453 areStatePropsEqual = shallowEqual,
454 areMergedPropsEqual = shallowEqual,
455
456 // use React's forwardRef to expose a ref of the wrapped component
457 forwardRef = false,
458
459 // the context consumer to use
460 context = ReactReduxContext,
461 }: ConnectOptions<unknown, unknown, unknown, unknown> = {},
462): unknown {
463 if (process.env.NODE_ENV !== 'production') {
464 if (pure !== undefined && !hasWarnedAboutDeprecatedPureOption) {
465 hasWarnedAboutDeprecatedPureOption = true
466 warning(
467 'The `pure` option has been removed. `connect` is now always a "pure/memoized" component',
468 )
469 }
470 }
471
472 const Context = context
473
474 const initMapStateToProps = mapStateToPropsFactory(mapStateToProps)
475 const initMapDispatchToProps = mapDispatchToPropsFactory(mapDispatchToProps)
476 const initMergeProps = mergePropsFactory(mergeProps)
477
478 const shouldHandleStateChanges = Boolean(mapStateToProps)
479
480 const wrapWithConnect = <TProps,>(
481 WrappedComponent: ComponentType<TProps>,
482 ) => {
483 type WrappedComponentProps = TProps &
484 ConnectPropsMaybeWithoutContext<TProps>
485
486 if (process.env.NODE_ENV !== 'production') {
487 const isValid = /*#__PURE__*/ isValidElementType(WrappedComponent)
488 if (!isValid)
489 throw new Error(
490 `You must pass a component to the function returned by connect. Instead received ${stringifyComponent(
491 WrappedComponent,
492 )}`,
493 )
494 }
495
496 const wrappedComponentName =
497 WrappedComponent.displayName || WrappedComponent.name || 'Component'
498
499 const displayName = `Connect(${wrappedComponentName})`
500
501 const selectorFactoryOptions: SelectorFactoryOptions<
502 any,
503 any,
504 any,
505 any,
506 State
507 > = {
508 shouldHandleStateChanges,
509 displayName,
510 wrappedComponentName,
511 WrappedComponent,
512 // @ts-ignore
513 initMapStateToProps,
514 initMapDispatchToProps,
515 initMergeProps,
516 areStatesEqual,
517 areStatePropsEqual,
518 areOwnPropsEqual,
519 areMergedPropsEqual,
520 }
521
522 function ConnectFunction<TOwnProps>(
523 props: InternalConnectProps & TOwnProps,
524 ) {
525 const [propsContext, reactReduxForwardedRef, wrapperProps] =
526 React.useMemo(() => {
527 // Distinguish between actual "data" props that were passed to the wrapper component,
528 // and values needed to control behavior (forwarded refs, alternate context instances).
529 // To maintain the wrapperProps object reference, memoize this destructuring.
530 const { reactReduxForwardedRef, ...wrapperProps } = props
531 return [props.context, reactReduxForwardedRef, wrapperProps]
532 }, [props])
533
534 const ContextToUse: ReactReduxContextInstance = React.useMemo(() => {
535 // Users may optionally pass in a custom context instance to use instead of our ReactReduxContext.
536 // Memoize the check that determines which context instance we should use.
537 let ResultContext = Context
538 if (propsContext?.Consumer) {
539 if (process.env.NODE_ENV !== 'production') {
540 const isValid = /*#__PURE__*/ isContextConsumer(
541 // @ts-ignore
542 <propsContext.Consumer />,
543 )
544 if (!isValid) {
545 throw new Error(
546 'You must pass a valid React context consumer as `props.context`',
547 )
548 }
549 ResultContext = propsContext
550 }
551 }
552 return ResultContext
553 }, [propsContext, Context])
554
555 // Retrieve the store and ancestor subscription via context, if available
556 const contextValue = React.useContext(ContextToUse)
557
558 // The store _must_ exist as either a prop or in context.
559 // We'll check to see if it _looks_ like a Redux store first.
560 // This allows us to pass through a `store` prop that is just a plain value.
561 const didStoreComeFromProps =
562 Boolean(props.store) &&
563 Boolean(props.store!.getState) &&
564 Boolean(props.store!.dispatch)
565 const didStoreComeFromContext =
566 Boolean(contextValue) && Boolean(contextValue!.store)
567
568 if (
569 process.env.NODE_ENV !== 'production' &&
570 !didStoreComeFromProps &&
571 !didStoreComeFromContext
572 ) {
573 throw new Error(
574 `Could not find "store" in the context of ` +
575 `"${displayName}". Either wrap the root component in a <Provider>, ` +
576 `or pass a custom React context provider to <Provider> and the corresponding ` +
577 `React context consumer to ${displayName} in connect options.`,
578 )
579 }
580
581 // Based on the previous check, one of these must be true
582 const store: Store = didStoreComeFromProps
583 ? props.store!
584 : contextValue!.store
585
586 const getServerState = didStoreComeFromContext
587 ? contextValue!.getServerState
588 : store.getState
589
590 const childPropsSelector = React.useMemo(() => {
591 // The child props selector needs the store reference as an input.
592 // Re-create this selector whenever the store changes.
593 return defaultSelectorFactory(store.dispatch, selectorFactoryOptions)
594 }, [store])
595
596 const [subscription, notifyNestedSubs] = React.useMemo(() => {
597 if (!shouldHandleStateChanges) return NO_SUBSCRIPTION_ARRAY
598
599 // This Subscription's source should match where store came from: props vs. context. A component
600 // connected to the store via props shouldn't use subscription from context, or vice versa.
601 const subscription = createSubscription(
602 store,
603 didStoreComeFromProps ? undefined : contextValue!.subscription,
604 )
605
606 // `notifyNestedSubs` is duplicated to handle the case where the component is unmounted in
607 // the middle of the notification loop, where `subscription` will then be null. This can
608 // probably be avoided if Subscription's listeners logic is changed to not call listeners
609 // that have been unsubscribed in the middle of the notification loop.
610 const notifyNestedSubs =
611 subscription.notifyNestedSubs.bind(subscription)
612
613 return [subscription, notifyNestedSubs]
614 }, [store, didStoreComeFromProps, contextValue])
615
616 // Determine what {store, subscription} value should be put into nested context, if necessary,
617 // and memoize that value to avoid unnecessary context updates.
618 const overriddenContextValue = React.useMemo(() => {
619 if (didStoreComeFromProps) {
620 // This component is directly subscribed to a store from props.
621 // We don't want descendants reading from this store - pass down whatever
622 // the existing context value is from the nearest connected ancestor.
623 return contextValue!
624 }
625
626 // Otherwise, put this component's subscription instance into context, so that
627 // connected descendants won't update until after this component is done
628 return {
629 ...contextValue,
630 subscription,
631 } as ReactReduxContextValue
632 }, [didStoreComeFromProps, contextValue, subscription])
633
634 // Set up refs to coordinate values between the subscription effect and the render logic
635 const lastChildProps = React.useRef<unknown>(undefined)
636 const lastWrapperProps = React.useRef(wrapperProps)
637 const childPropsFromStoreUpdate = React.useRef<unknown>(undefined)
638 const renderIsScheduled = React.useRef(false)
639 const isMounted = React.useRef(false)
640
641 // TODO: Change this to `React.useRef<Error>(undefined)` after upgrading to React 19.
642 /**
643 * @todo Change this to `React.useRef<Error>(undefined)` after upgrading to React 19.
644 */
645 const latestSubscriptionCallbackError = React.useRef<Error | undefined>(
646 undefined,
647 )
648
649 useIsomorphicLayoutEffect(() => {
650 isMounted.current = true
651 return () => {
652 isMounted.current = false
653 }
654 }, [])
655
656 const actualChildPropsSelector = React.useMemo(() => {
657 const selector = () => {
658 // Tricky logic here:
659 // - This render may have been triggered by a Redux store update that produced new child props
660 // - However, we may have gotten new wrapper props after that
661 // If we have new child props, and the same wrapper props, we know we should use the new child props as-is.
662 // But, if we have new wrapper props, those might change the child props, so we have to recalculate things.
663 // So, we'll use the child props from store update only if the wrapper props are the same as last time.
664 if (
665 childPropsFromStoreUpdate.current &&
666 wrapperProps === lastWrapperProps.current
667 ) {
668 return childPropsFromStoreUpdate.current
669 }
670
671 // TODO We're reading the store directly in render() here. Bad idea?
672 // This will likely cause Bad Things (TM) to happen in Concurrent Mode.
673 // Note that we do this because on renders _not_ caused by store updates, we need the latest store state
674 // to determine what the child props should be.
675 return childPropsSelector(store.getState(), wrapperProps)
676 }
677 return selector
678 }, [store, wrapperProps])
679
680 // We need this to execute synchronously every time we re-render. However, React warns
681 // about useLayoutEffect in SSR, so we try to detect environment and fall back to
682 // just useEffect instead to avoid the warning, since neither will run anyway.
683
684 const subscribeForReact = React.useMemo(() => {
685 const subscribe = (reactListener: () => void) => {
686 if (!subscription) {
687 return () => {}
688 }
689
690 return subscribeUpdates(
691 shouldHandleStateChanges,
692 store,
693 subscription,
694 // @ts-ignore
695 childPropsSelector,
696 lastWrapperProps,
697 lastChildProps,
698 renderIsScheduled,
699 isMounted,
700 childPropsFromStoreUpdate,
701 notifyNestedSubs,
702 reactListener,
703 )
704 }
705
706 return subscribe
707 }, [subscription])
708
709 useIsomorphicLayoutEffectWithArgs(captureWrapperProps, [
710 lastWrapperProps,
711 lastChildProps,
712 renderIsScheduled,
713 wrapperProps,
714 childPropsFromStoreUpdate,
715 notifyNestedSubs,
716 ])
717
718 let actualChildProps: Record<string, unknown>
719
720 try {
721 actualChildProps = React.useSyncExternalStore(
722 // TODO We're passing through a big wrapper that does a bunch of extra side effects besides subscribing
723 subscribeForReact,
724 // TODO This is incredibly hacky. We've already processed the store update and calculated new child props,
725 // TODO and we're just passing that through so it triggers a re-render for us rather than relying on `uSES`.
726 actualChildPropsSelector,
727 getServerState
728 ? () => childPropsSelector(getServerState(), wrapperProps)
729 : actualChildPropsSelector,
730 )
731 } catch (err) {
732 if (latestSubscriptionCallbackError.current) {
733 // eslint-disable-next-line no-extra-semi
734 ;(err as Error).message +=
735 `\nThe error may be correlated with this previous error:\n${latestSubscriptionCallbackError.current.stack}\n\n`
736 }
737
738 throw err
739 }
740
741 useIsomorphicLayoutEffect(() => {
742 latestSubscriptionCallbackError.current = undefined
743 childPropsFromStoreUpdate.current = undefined
744 lastChildProps.current = actualChildProps
745 })
746
747 // Now that all that's done, we can finally try to actually render the child component.
748 // We memoize the elements for the rendered child component as an optimization.
749 const renderedWrappedComponent = React.useMemo(() => {
750 return (
751 // @ts-ignore
752 <WrappedComponent
753 {...actualChildProps}
754 ref={reactReduxForwardedRef}
755 />
756 )
757 }, [reactReduxForwardedRef, WrappedComponent, actualChildProps])
758
759 // If React sees the exact same element reference as last time, it bails out of re-rendering
760 // that child, same as if it was wrapped in React.memo() or returned false from shouldComponentUpdate.
761 const renderedChild = React.useMemo(() => {
762 if (shouldHandleStateChanges) {
763 // If this component is subscribed to store updates, we need to pass its own
764 // subscription instance down to our descendants. That means rendering the same
765 // Context instance, and putting a different value into the context.
766 return (
767 <ContextToUse.Provider value={overriddenContextValue}>
768 {renderedWrappedComponent}
769 </ContextToUse.Provider>
770 )
771 }
772
773 return renderedWrappedComponent
774 }, [ContextToUse, renderedWrappedComponent, overriddenContextValue])
775
776 return renderedChild
777 }
778
779 const _Connect = React.memo(ConnectFunction)
780
781 type ConnectedWrapperComponent = typeof _Connect & {
782 WrappedComponent: typeof WrappedComponent
783 }
784
785 // Add a hacky cast to get the right output type
786 const Connect = _Connect as unknown as ConnectedComponent<
787 typeof WrappedComponent,
788 WrappedComponentProps
789 >
790 Connect.WrappedComponent = WrappedComponent
791 Connect.displayName = ConnectFunction.displayName = displayName
792
793 if (forwardRef) {
794 const _forwarded = React.forwardRef(
795 function forwardConnectRef(props, ref) {
796 // @ts-ignore
797 return <Connect {...props} reactReduxForwardedRef={ref} />
798 },
799 )
800
801 const forwarded = _forwarded as ConnectedWrapperComponent
802 forwarded.displayName = displayName
803 forwarded.WrappedComponent = WrappedComponent
804 return /*#__PURE__*/ hoistStatics(forwarded, WrappedComponent)
805 }
806
807 return /*#__PURE__*/ hoistStatics(Connect, WrappedComponent)
808 }
809
810 return wrapWithConnect
811}
812
813export default connect as Connect
Note: See TracBrowser for help on using the repository browser.