@@ -6,6 +6,8 @@ import React, {
6
6
useLayoutEffect ,
7
7
useState ,
8
8
useContext ,
9
+ Suspense ,
10
+ useEffect ,
9
11
} from 'react'
10
12
import { createStore } from 'redux'
11
13
import * as rtl from '@testing-library/react'
@@ -723,6 +725,130 @@ describe('React', () => {
723
725
const expectedMaxUnmountTime = IS_REACT_18 ? 500 : 7000
724
726
expect ( elapsedTime ) . toBeLessThan ( expectedMaxUnmountTime )
725
727
} )
728
+
729
+ it ( 'keeps working when used inside a Suspense' , async ( ) => {
730
+ let result : number | undefined
731
+ let expectedResult : number | undefined
732
+ let lazyComponentAdded = false
733
+ let lazyComponentLoaded = false
734
+
735
+ // A lazy loaded component in the Suspense
736
+ // This component does nothing really. It is lazy loaded to trigger the issue
737
+ // Lazy loading this component will break other useSelectors in the same Suspense
738
+ // See issue https://github.com/reduxjs/react-redux/issues/1977
739
+ const OtherComp = ( ) => {
740
+ useLayoutEffect ( ( ) => {
741
+ lazyComponentLoaded = true
742
+ } , [ ] )
743
+
744
+ return < div > </ div >
745
+ }
746
+ let otherCompFinishLoading : ( ) => void = ( ) => { }
747
+ const OtherComponentLazy = React . lazy (
748
+ ( ) =>
749
+ new Promise < { default : React . ComponentType < any > } > ( ( resolve ) => {
750
+ otherCompFinishLoading = ( ) =>
751
+ resolve ( {
752
+ default : OtherComp ,
753
+ } )
754
+ } )
755
+ )
756
+ let addOtherComponent : ( ) => void = ( ) => { }
757
+ const Dispatcher = React . memo ( ( ) => {
758
+ const [ load , setLoad ] = useState ( false )
759
+
760
+ useEffect ( ( ) => {
761
+ addOtherComponent = ( ) => setLoad ( true )
762
+ } , [ ] )
763
+ useEffect ( ( ) => {
764
+ lazyComponentAdded = true
765
+ } )
766
+ return load ? < OtherComponentLazy /> : null
767
+ } )
768
+ // End of lazy loading component
769
+
770
+ // The test component inside the suspense (uses the useSelector which breaks)
771
+ const CompInsideSuspense = ( ) => {
772
+ const count = useNormalSelector ( ( state ) => state . count )
773
+
774
+ result = count
775
+ return (
776
+ < div >
777
+ { count }
778
+ < Dispatcher />
779
+ </ div >
780
+ )
781
+ }
782
+ // The test component outside the suspense (uses the useSelector which keeps working - for comparison)
783
+ const CompOutsideSuspense = ( ) => {
784
+ const count = useNormalSelector ( ( state ) => state . count )
785
+
786
+ expectedResult = count
787
+ return < div > { count } </ div >
788
+ }
789
+
790
+ // Now, steps to reproduce
791
+ // step 1. make sure the component with the useSelector inside the Suspsense is rendered
792
+ // -> it will register the subscription
793
+ // step 2. make sure the suspense is switched back to "Loading..." state by adding a component
794
+ // -> this will remove our useSelector component from the page temporary!
795
+ // step 3. Finish loading the other component, so the suspense is no longer loading
796
+ // -> this will re-add our <Provider> and useSelector component
797
+ // step 4. Check that the useSelectors in our re-added components still work
798
+
799
+ // step 1: render will render our component with the useSelector
800
+ rtl . render (
801
+ < >
802
+ < Suspense fallback = { < div > Loading... </ div > } >
803
+ < ProviderMock store = { normalStore } >
804
+ < CompInsideSuspense />
805
+ </ ProviderMock >
806
+ </ Suspense >
807
+ < ProviderMock store = { normalStore } >
808
+ < CompOutsideSuspense />
809
+ </ ProviderMock >
810
+ </ >
811
+ )
812
+
813
+ // step 2: make sure the suspense is switched back to "Loading..." state by adding a component
814
+ rtl . act ( ( ) => {
815
+ addOtherComponent ( )
816
+ } )
817
+ await rtl . waitFor ( ( ) => {
818
+ if ( ! lazyComponentAdded ) {
819
+ throw new Error ( 'Suspense is not back in loading yet' )
820
+ }
821
+ } )
822
+ expect ( lazyComponentAdded ) . toEqual ( true )
823
+
824
+ // step 3. Finish loading the other component, so the suspense is no longer loading
825
+ // This will re-add our components under the suspense, but will NOT rerender them!
826
+ rtl . act ( ( ) => {
827
+ otherCompFinishLoading ( )
828
+ } )
829
+ await rtl . waitFor ( ( ) => {
830
+ if ( ! lazyComponentLoaded ) {
831
+ throw new Error ( 'Suspense is not back to loaded yet' )
832
+ }
833
+ } )
834
+ expect ( lazyComponentLoaded ) . toEqual ( true )
835
+
836
+ // step 4. Check that the useSelectors in our re-added components still work
837
+ // Do an update to the redux store
838
+ rtl . act ( ( ) => {
839
+ normalStore . dispatch ( { type : '' } )
840
+ } )
841
+
842
+ // Check the component *outside* the Suspense to check whether React rerendered
843
+ await rtl . waitFor ( ( ) => {
844
+ if ( expectedResult !== 1 ) {
845
+ throw new Error ( 'useSelector did not return 1 yet' )
846
+ }
847
+ } )
848
+
849
+ // Expect the useSelector *inside* the Suspense to also update (this was broken)
850
+ expect ( result ) . toEqual ( expectedResult )
851
+ } )
726
852
} )
727
853
728
854
describe ( 'error handling for invalid arguments' , ( ) => {
@@ -805,6 +931,7 @@ describe('React', () => {
805
931
} ) ,
806
932
selected : expect . any ( Number ) ,
807
933
selected2 : expect . any ( Number ) ,
934
+ stack : expect . any ( String ) ,
808
935
} )
809
936
)
810
937
} )
@@ -920,7 +1047,10 @@ describe('React', () => {
920
1047
)
921
1048
922
1049
expect ( consoleSpy ) . toHaveBeenCalledWith (
923
- expect . stringContaining ( 'returned the root state when called.' )
1050
+ expect . stringContaining ( 'returned the root state when called.' ) ,
1051
+ expect . objectContaining ( {
1052
+ stack : expect . any ( String ) ,
1053
+ } )
924
1054
)
925
1055
} )
926
1056
} )
0 commit comments