From 4eeee358e12c1408a4b40830bb7bb6956cf26b47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Thu, 17 Oct 2019 15:57:07 -0700 Subject: [PATCH] [SuspenseList] Store lastEffect before rendering (#17131) * Add a failing test for SuspenseList bug * Store lastEffect before rendering We can't reset the effect list to null because we don't rereconcile the children so we drop deletion effects if we do that. Instead we store the last effect as it was before we started rendering so we can go back to where it was when we reset it. We actually already do something like this when we delete the last row for the tail="hidden" mode so we had a field available for it already. --- .../src/ReactFiberBeginWork.js | 10 ++- .../src/ReactFiberCompleteWork.js | 5 +- .../ReactSuspenseList-test.internal.js | 84 +++++++++++++++++++ 3 files changed, 96 insertions(+), 3 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index a6ee56a09061e..a207b5e9ef05c 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -2355,18 +2355,20 @@ function initSuspenseListRenderState( tail: null | Fiber, lastContentRow: null | Fiber, tailMode: SuspenseListTailMode, + lastEffectBeforeRendering: null | Fiber, ): void { let renderState: null | SuspenseListRenderState = workInProgress.memoizedState; if (renderState === null) { - workInProgress.memoizedState = { + workInProgress.memoizedState = ({ isBackwards: isBackwards, rendering: null, last: lastContentRow, tail: tail, tailExpiration: 0, tailMode: tailMode, - }; + lastEffect: lastEffectBeforeRendering, + }: SuspenseListRenderState); } else { // We can reuse the existing object from previous renders. renderState.isBackwards = isBackwards; @@ -2375,6 +2377,7 @@ function initSuspenseListRenderState( renderState.tail = tail; renderState.tailExpiration = 0; renderState.tailMode = tailMode; + renderState.lastEffect = lastEffectBeforeRendering; } } @@ -2456,6 +2459,7 @@ function updateSuspenseListComponent( tail, lastContentRow, tailMode, + workInProgress.lastEffect, ); break; } @@ -2487,6 +2491,7 @@ function updateSuspenseListComponent( tail, null, // last tailMode, + workInProgress.lastEffect, ); break; } @@ -2497,6 +2502,7 @@ function updateSuspenseListComponent( null, // tail null, // last undefined, + workInProgress.lastEffect, ); break; } diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index 2436ec6af6a63..8bb417723fdfc 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -1053,7 +1053,10 @@ function completeWork( // Rerender the whole list, but this time, we'll force fallbacks // to stay in place. // Reset the effect list before doing the second pass since that's now invalid. - workInProgress.firstEffect = workInProgress.lastEffect = null; + if (renderState.lastEffect === null) { + workInProgress.firstEffect = null; + } + workInProgress.lastEffect = renderState.lastEffect; // Reset the child fibers to their original state. resetChildFibers(workInProgress, renderExpirationTime); diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.internal.js index e22dc8ccd63cb..f41e7a5073eed 100644 --- a/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactSuspenseList-test.internal.js @@ -585,6 +585,90 @@ describe('ReactSuspenseList', () => { ); }); + it('displays all "together" during an update', async () => { + let A = createAsyncText('A'); + let B = createAsyncText('B'); + let C = createAsyncText('C'); + let D = createAsyncText('D'); + + function Foo({step}) { + return ( + + {step === 0 && ( + }> + + + )} + {step === 0 && ( + }> + + + )} + {step === 1 && ( + }> + + + )} + {step === 1 && ( + }> + + + )} + + ); + } + + // Mount + await A.resolve(); + ReactNoop.render(); + expect(Scheduler).toFlushAndYield([ + 'A', + 'Suspend! [B]', + 'Loading B', + 'Loading A', + 'Loading B', + ]); + expect(ReactNoop).toMatchRenderedOutput( + <> + Loading A + Loading B + , + ); + await B.resolve(); + expect(Scheduler).toFlushAndYield(['A', 'B']); + expect(ReactNoop).toMatchRenderedOutput( + <> + A + B + , + ); + + // Update + await C.resolve(); + ReactNoop.render(); + expect(Scheduler).toFlushAndYield([ + 'C', + 'Suspend! [D]', + 'Loading D', + 'Loading C', + 'Loading D', + ]); + expect(ReactNoop).toMatchRenderedOutput( + <> + Loading C + Loading D + , + ); + await D.resolve(); + expect(Scheduler).toFlushAndYield(['C', 'D']); + expect(ReactNoop).toMatchRenderedOutput( + <> + C + D + , + ); + }); + it('avoided boundaries can be coordinate with SuspenseList', async () => { let A = createAsyncText('A'); let B = createAsyncText('B');