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