diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 1d5ac8fcc4bc..ba99ac80abce 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -3153,6 +3153,7 @@ function beginWork( // update in the past but didn't complete it. renderState.rendering = null; renderState.tail = null; + renderState.lastEffect = null; } pushSuspenseContext(workInProgress, suspenseStackCursor.current); diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.internal.js index 7928ca491b08..c13c31144828 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.internal.js @@ -2300,4 +2300,189 @@ describe('ReactSuspenseList', () => { // remount. expect(previousInst).toBe(setAsyncB); }); + + it('is able to re-suspend the last rows during an update with hidden', async () => { + let AsyncB = createAsyncText('B'); + + let setAsyncB; + + function B() { + let [shouldBeAsync, setAsync] = React.useState(false); + setAsyncB = setAsync; + + return shouldBeAsync ? ( + }> + + + ) : ( + + ); + } + + function Foo({updateList}) { + return ( + + }> + + + + + ); + } + + ReactNoop.render(); + + expect(Scheduler).toFlushAndYield(['A', 'Sync B']); + + expect(ReactNoop).toMatchRenderedOutput( + <> + A + Sync B + , + ); + + let previousInst = setAsyncB; + + // During an update we suspend on B. + ReactNoop.act(() => setAsyncB(true)); + + expect(Scheduler).toHaveYielded([ + 'Suspend! [B]', + 'Loading B', + // The second pass is the "force hide" pass + 'Loading B', + ]); + + expect(ReactNoop).toMatchRenderedOutput( + <> + A + Loading B + , + ); + + // Before we resolve we'll rerender the whole list. + // This should leave the tree intact. + ReactNoop.act(() => ReactNoop.render()); + + expect(Scheduler).toHaveYielded(['A', 'Suspend! [B]', 'Loading B']); + + expect(ReactNoop).toMatchRenderedOutput( + <> + A + Loading B + , + ); + + await AsyncB.resolve(); + + expect(Scheduler).toFlushAndYield(['B']); + + expect(ReactNoop).toMatchRenderedOutput( + <> + A + B + , + ); + + // This should be the same instance. I.e. it didn't + // remount. + expect(previousInst).toBe(setAsyncB); + }); + + it('is able to interrupt a partially rendered tree and continue later', async () => { + let AsyncA = createAsyncText('A'); + + let updateLowPri; + let updateHighPri; + + function Bar() { + let [highPriState, setHighPriState] = React.useState(false); + updateHighPri = setHighPriState; + return highPriState ? : null; + } + + function Foo() { + let [lowPriState, setLowPriState] = React.useState(false); + updateLowPri = setLowPriState; + return ( + + }> + + + {lowPriState ? : null} + {lowPriState ? : null} + {lowPriState ? : null} + + ); + } + + ReactNoop.render(); + + expect(Scheduler).toFlushAndYield([]); + + expect(ReactNoop).toMatchRenderedOutput(null); + + ReactNoop.act(() => { + // Add a few items at the end. + updateLowPri(true); + + // Flush partially through. + expect(Scheduler).toFlushAndYieldThrough(['B', 'C']); + + // Schedule another update at higher priority. + Scheduler.unstable_runWithPriority( + Scheduler.unstable_UserBlockingPriority, + () => updateHighPri(true), + ); + + // That will intercept the previous render. + }); + + jest.runAllTimers(); + + expect(Scheduler).toHaveYielded( + __DEV__ + ? [ + // First attempt at high pri. + 'Suspend! [A]', + 'Loading A', + // Re-render at forced. + 'Suspend! [A]', + 'Loading A', + // We auto-commit this on DEV. + // Try again on low-pri. + 'Suspend! [A]', + 'Loading A', + ] + : [ + // First attempt at high pri. + 'Suspend! [A]', + 'Loading A', + // Re-render at forced. + 'Suspend! [A]', + 'Loading A', + // We didn't commit so retry at low-pri. + 'Suspend! [A]', + 'Loading A', + // Re-render at forced. + 'Suspend! [A]', + 'Loading A', + ], + ); + + expect(ReactNoop).toMatchRenderedOutput(Loading A); + + await AsyncA.resolve(); + + expect(Scheduler).toFlushAndYield(['A', 'B', 'C', 'D']); + + expect(ReactNoop).toMatchRenderedOutput( + <> + A + B + C + D + , + ); + }); });