2
2
import hoistStatics from 'hoist-non-react-statics'
3
3
import React , { useContext , useMemo , useRef , useReducer } from 'react'
4
4
import { isValidElementType , isContextConsumer } from 'react-is'
5
+ import { useSyncExternalStore } from 'use-sync-external-store'
6
+
5
7
import type { Store , Dispatch , Action , AnyAction } from 'redux'
6
8
7
9
import type {
@@ -49,19 +51,6 @@ const stringifyComponent = (Comp: unknown) => {
49
51
}
50
52
}
51
53
52
- // Reducer for our "forceUpdate" equivalent.
53
- // This primarily stores the current error, if any,
54
- // but also an update counter.
55
- // Since we're returning a new array anyway, in theory the counter isn't needed.
56
- // Or for that matter, since the dispatch gets a new object, we don't even need an array.
57
- function storeStateUpdatesReducer (
58
- state : [ unknown , number ] ,
59
- action : { payload : unknown }
60
- ) {
61
- const [ , updateCount ] = state
62
- return [ action . payload , updateCount + 1 ]
63
- }
64
-
65
54
type EffectFunc = ( ...args : any [ ] ) => void | ReturnType < React . EffectCallback >
66
55
67
56
// This is "just" a `useLayoutEffect`, but with two modifications:
@@ -82,13 +71,12 @@ function captureWrapperProps(
82
71
lastChildProps : React . MutableRefObject < unknown > ,
83
72
renderIsScheduled : React . MutableRefObject < boolean > ,
84
73
wrapperProps : unknown ,
85
- actualChildProps : unknown ,
74
+ // actualChildProps: unknown,
86
75
childPropsFromStoreUpdate : React . MutableRefObject < unknown > ,
87
76
notifyNestedSubs : ( ) => void
88
77
) {
89
78
// We want to capture the wrapper props and child props we used for later comparisons
90
79
lastWrapperProps . current = wrapperProps
91
- lastChildProps . current = actualChildProps
92
80
renderIsScheduled . current = false
93
81
94
82
// If the render was from a store update, clear out that reference and cascade the subscriber update
@@ -108,20 +96,22 @@ function subscribeUpdates(
108
96
lastWrapperProps : React . MutableRefObject < unknown > ,
109
97
lastChildProps : React . MutableRefObject < unknown > ,
110
98
renderIsScheduled : React . MutableRefObject < boolean > ,
99
+ isMounted : React . MutableRefObject < boolean > ,
111
100
childPropsFromStoreUpdate : React . MutableRefObject < unknown > ,
112
101
notifyNestedSubs : ( ) => void ,
113
- forceComponentUpdateDispatch : React . Dispatch < any >
102
+ // forceComponentUpdateDispatch: React.Dispatch<any>,
103
+ additionalSubscribeListener : ( ) => void
114
104
) {
115
105
// If we're not subscribed to the store, nothing to do here
116
- if ( ! shouldHandleStateChanges ) return
106
+ if ( ! shouldHandleStateChanges ) return ( ) => { }
117
107
118
108
// Capture values for checking if and when this component unmounts
119
109
let didUnsubscribe = false
120
110
let lastThrownError : Error | null = null
121
111
122
112
// We'll run this callback every time a store subscription update propagates to this component
123
113
const checkForUpdates = ( ) => {
124
- if ( didUnsubscribe ) {
114
+ if ( didUnsubscribe || ! isMounted . current ) {
125
115
// Don't run stale listeners.
126
116
// Redux doesn't guarantee unsubscriptions happen until next dispatch.
127
117
return
@@ -160,13 +150,8 @@ function subscribeUpdates(
160
150
childPropsFromStoreUpdate . current = newChildProps
161
151
renderIsScheduled . current = true
162
152
163
- // If the child props _did_ change (or we caught an error), this wrapper component needs to re-render
164
- forceComponentUpdateDispatch ( {
165
- type : 'STORE_UPDATED' ,
166
- payload : {
167
- error,
168
- } ,
169
- } )
153
+ // Trigger the React `useSyncExternalStore` subscriber
154
+ additionalSubscribeListener ( )
170
155
}
171
156
}
172
157
@@ -555,9 +540,7 @@ function connect<
555
540
// If we aren't running in "pure" mode, we don't want to memoize values.
556
541
// To avoid conditionally calling hooks, we fall back to a tiny wrapper
557
542
// that just executes the given callback immediately.
558
- const usePureOnlyMemo = pure
559
- ? useMemo
560
- : ( callback : ( ) => void ) => callback ( )
543
+ const usePureOnlyMemo = pure ? useMemo : ( callback : ( ) => any ) => callback ( )
561
544
562
545
function ConnectFunction < TOwnProps > ( props : ConnectProps & TOwnProps ) {
563
546
const [ propsContext , reactReduxForwardedRef , wrapperProps ] =
@@ -655,91 +638,119 @@ function connect<
655
638
} as ReactReduxContextValue
656
639
} , [ didStoreComeFromProps , contextValue , subscription ] )
657
640
658
- // We need to force this wrapper component to re-render whenever a Redux store update
659
- // causes a change to the calculated child component props (or we caught an error in mapState)
660
- const [ [ previousStateUpdateResult ] , forceComponentUpdateDispatch ] =
661
- useReducer (
662
- storeStateUpdatesReducer ,
663
- // @ts -ignore
664
- EMPTY_ARRAY as any ,
665
- initStateUpdates
666
- )
667
-
668
- // Propagate any mapState/mapDispatch errors upwards
669
- if ( previousStateUpdateResult && previousStateUpdateResult . error ) {
670
- throw previousStateUpdateResult . error
671
- }
672
-
673
641
// Set up refs to coordinate values between the subscription effect and the render logic
674
- const lastChildProps = useRef ( )
642
+ const lastChildProps = useRef < unknown > ( )
675
643
const lastWrapperProps = useRef ( wrapperProps )
676
- const childPropsFromStoreUpdate = useRef ( )
644
+ const childPropsFromStoreUpdate = useRef < unknown > ( )
677
645
const renderIsScheduled = useRef ( false )
646
+ const isProcessingDispatch = useRef ( false )
647
+ const isMounted = useRef ( false )
678
648
679
- const actualChildProps = usePureOnlyMemo ( ( ) => {
680
- // Tricky logic here:
681
- // - This render may have been triggered by a Redux store update that produced new child props
682
- // - However, we may have gotten new wrapper props after that
683
- // If we have new child props, and the same wrapper props, we know we should use the new child props as-is.
684
- // But, if we have new wrapper props, those might change the child props, so we have to recalculate things.
685
- // So, we'll use the child props from store update only if the wrapper props are the same as last time.
686
- if (
687
- childPropsFromStoreUpdate . current &&
688
- wrapperProps === lastWrapperProps . current
689
- ) {
690
- return childPropsFromStoreUpdate . current
691
- }
649
+ const latestSubscriptionCallbackError = useRef < Error > ( )
692
650
693
- // TODO We're reading the store directly in render() here. Bad idea?
694
- // This will likely cause Bad Things (TM) to happen in Concurrent Mode.
695
- // Note that we do this because on renders _not_ caused by store updates, we need the latest store state
696
- // to determine what the child props should be.
697
- return childPropsSelector ( store . getState ( ) , wrapperProps )
698
- } , [ store , previousStateUpdateResult , wrapperProps ] )
651
+ useIsomorphicLayoutEffect ( ( ) => {
652
+ isMounted . current = true
653
+ return ( ) => {
654
+ isMounted . current = false
655
+ }
656
+ } , [ ] )
657
+
658
+ const actualChildPropsSelector = usePureOnlyMemo ( ( ) => {
659
+ const selector = ( ) => {
660
+ // Tricky logic here:
661
+ // - This render may have been triggered by a Redux store update that produced new child props
662
+ // - However, we may have gotten new wrapper props after that
663
+ // If we have new child props, and the same wrapper props, we know we should use the new child props as-is.
664
+ // But, if we have new wrapper props, those might change the child props, so we have to recalculate things.
665
+ // So, we'll use the child props from store update only if the wrapper props are the same as last time.
666
+ if (
667
+ childPropsFromStoreUpdate . current &&
668
+ wrapperProps === lastWrapperProps . current
669
+ ) {
670
+ return childPropsFromStoreUpdate . current
671
+ }
672
+
673
+ // TODO We're reading the store directly in render() here. Bad idea?
674
+ // This will likely cause Bad Things (TM) to happen in Concurrent Mode.
675
+ // Note that we do this because on renders _not_ caused by store updates, we need the latest store state
676
+ // to determine what the child props should be.
677
+ return childPropsSelector ( store . getState ( ) , wrapperProps )
678
+ }
679
+ return selector
680
+ } , [ store , wrapperProps ] )
699
681
700
682
// We need this to execute synchronously every time we re-render. However, React warns
701
683
// about useLayoutEffect in SSR, so we try to detect environment and fall back to
702
684
// just useEffect instead to avoid the warning, since neither will run anyway.
685
+
686
+ const subscribeForReact = useMemo ( ( ) => {
687
+ const subscribe = ( reactListener : ( ) => void ) => {
688
+ if ( ! subscription ) {
689
+ return ( ) => { }
690
+ }
691
+
692
+ return subscribeUpdates (
693
+ shouldHandleStateChanges ,
694
+ store ,
695
+ subscription ,
696
+ // @ts -ignore
697
+ childPropsSelector ,
698
+ lastWrapperProps ,
699
+ lastChildProps ,
700
+ renderIsScheduled ,
701
+ isMounted ,
702
+ childPropsFromStoreUpdate ,
703
+ notifyNestedSubs ,
704
+ reactListener
705
+ )
706
+ }
707
+
708
+ return subscribe
709
+ } , [ subscription ] )
710
+
703
711
useIsomorphicLayoutEffectWithArgs ( captureWrapperProps , [
704
712
lastWrapperProps ,
705
713
lastChildProps ,
706
714
renderIsScheduled ,
707
715
wrapperProps ,
708
- actualChildProps ,
709
716
childPropsFromStoreUpdate ,
710
717
notifyNestedSubs ,
711
718
] )
712
719
713
- // Our re-subscribe logic only runs when the store/subscription setup changes
714
- useIsomorphicLayoutEffectWithArgs (
715
- subscribeUpdates ,
716
- [
717
- shouldHandleStateChanges ,
718
- store ,
719
- subscription ,
720
- childPropsSelector ,
721
- lastWrapperProps ,
722
- lastChildProps ,
723
- renderIsScheduled ,
724
- childPropsFromStoreUpdate ,
725
- notifyNestedSubs ,
726
- forceComponentUpdateDispatch ,
727
- ] ,
728
- [ store , subscription , childPropsSelector ]
729
- )
720
+ let actualChildProps : unknown
721
+
722
+ try {
723
+ actualChildProps = useSyncExternalStore (
724
+ subscribeForReact ,
725
+ actualChildPropsSelector
726
+ )
727
+ } catch ( err ) {
728
+ if ( latestSubscriptionCallbackError . current ) {
729
+ ; (
730
+ err as Error
731
+ ) . message += `\nThe error may be correlated with this previous error:\n${ latestSubscriptionCallbackError . current . stack } \n\n`
732
+ }
733
+
734
+ throw err
735
+ }
736
+
737
+ useIsomorphicLayoutEffect ( ( ) => {
738
+ latestSubscriptionCallbackError . current = undefined
739
+ childPropsFromStoreUpdate . current = undefined
740
+ lastChildProps . current = actualChildProps
741
+ } )
730
742
731
743
// Now that all that's done, we can finally try to actually render the child component.
732
744
// We memoize the elements for the rendered child component as an optimization.
733
- const renderedWrappedComponent = useMemo (
734
- ( ) => (
745
+ const renderedWrappedComponent = useMemo ( ( ) => {
746
+ return (
735
747
// @ts -ignore
736
748
< WrappedComponent
737
749
{ ...actualChildProps }
738
750
ref = { reactReduxForwardedRef }
739
751
/>
740
- ) ,
741
- [ reactReduxForwardedRef , WrappedComponent , actualChildProps ]
742
- )
752
+ )
753
+ } , [ reactReduxForwardedRef , WrappedComponent , actualChildProps ] )
743
754
744
755
// If React sees the exact same element reference as last time, it bails out of re-rendering
745
756
// that child, same as if it was wrapped in React.memo() or returned false from shouldComponentUpdate.
0 commit comments