@@ -6,6 +6,8 @@ import React, {
66 useLayoutEffect ,
77 useState ,
88 useContext ,
9+ Suspense ,
10+ useEffect ,
911} from 'react'
1012import { createStore } from 'redux'
1113import * as rtl from '@testing-library/react'
@@ -723,6 +725,130 @@ describe('React', () => {
723725 const expectedMaxUnmountTime = IS_REACT_18 ? 500 : 7000
724726 expect ( elapsedTime ) . toBeLessThan ( expectedMaxUnmountTime )
725727 } )
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+ } )
726852 } )
727853
728854 describe ( 'error handling for invalid arguments' , ( ) => {
0 commit comments