Skip to content

Commit 4eeee35

Browse files
authored
[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.
1 parent 4fb5bf6 commit 4eeee35

File tree

3 files changed

+96
-3
lines changed

3 files changed

+96
-3
lines changed

packages/react-reconciler/src/ReactFiberBeginWork.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2355,18 +2355,20 @@ function initSuspenseListRenderState(
23552355
tail: null | Fiber,
23562356
lastContentRow: null | Fiber,
23572357
tailMode: SuspenseListTailMode,
2358+
lastEffectBeforeRendering: null | Fiber,
23582359
): void {
23592360
let renderState: null | SuspenseListRenderState =
23602361
workInProgress.memoizedState;
23612362
if (renderState === null) {
2362-
workInProgress.memoizedState = {
2363+
workInProgress.memoizedState = ({
23632364
isBackwards: isBackwards,
23642365
rendering: null,
23652366
last: lastContentRow,
23662367
tail: tail,
23672368
tailExpiration: 0,
23682369
tailMode: tailMode,
2369-
};
2370+
lastEffect: lastEffectBeforeRendering,
2371+
}: SuspenseListRenderState);
23702372
} else {
23712373
// We can reuse the existing object from previous renders.
23722374
renderState.isBackwards = isBackwards;
@@ -2375,6 +2377,7 @@ function initSuspenseListRenderState(
23752377
renderState.tail = tail;
23762378
renderState.tailExpiration = 0;
23772379
renderState.tailMode = tailMode;
2380+
renderState.lastEffect = lastEffectBeforeRendering;
23782381
}
23792382
}
23802383

@@ -2456,6 +2459,7 @@ function updateSuspenseListComponent(
24562459
tail,
24572460
lastContentRow,
24582461
tailMode,
2462+
workInProgress.lastEffect,
24592463
);
24602464
break;
24612465
}
@@ -2487,6 +2491,7 @@ function updateSuspenseListComponent(
24872491
tail,
24882492
null, // last
24892493
tailMode,
2494+
workInProgress.lastEffect,
24902495
);
24912496
break;
24922497
}
@@ -2497,6 +2502,7 @@ function updateSuspenseListComponent(
24972502
null, // tail
24982503
null, // last
24992504
undefined,
2505+
workInProgress.lastEffect,
25002506
);
25012507
break;
25022508
}

packages/react-reconciler/src/ReactFiberCompleteWork.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1053,7 +1053,10 @@ function completeWork(
10531053
// Rerender the whole list, but this time, we'll force fallbacks
10541054
// to stay in place.
10551055
// Reset the effect list before doing the second pass since that's now invalid.
1056-
workInProgress.firstEffect = workInProgress.lastEffect = null;
1056+
if (renderState.lastEffect === null) {
1057+
workInProgress.firstEffect = null;
1058+
}
1059+
workInProgress.lastEffect = renderState.lastEffect;
10571060
// Reset the child fibers to their original state.
10581061
resetChildFibers(workInProgress, renderExpirationTime);
10591062

packages/react-reconciler/src/__tests__/ReactSuspenseList-test.internal.js

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,90 @@ describe('ReactSuspenseList', () => {
585585
);
586586
});
587587

588+
it('displays all "together" during an update', async () => {
589+
let A = createAsyncText('A');
590+
let B = createAsyncText('B');
591+
let C = createAsyncText('C');
592+
let D = createAsyncText('D');
593+
594+
function Foo({step}) {
595+
return (
596+
<SuspenseList revealOrder="together">
597+
{step === 0 && (
598+
<Suspense fallback={<Text text="Loading A" />}>
599+
<A />
600+
</Suspense>
601+
)}
602+
{step === 0 && (
603+
<Suspense fallback={<Text text="Loading B" />}>
604+
<B />
605+
</Suspense>
606+
)}
607+
{step === 1 && (
608+
<Suspense fallback={<Text text="Loading C" />}>
609+
<C />
610+
</Suspense>
611+
)}
612+
{step === 1 && (
613+
<Suspense fallback={<Text text="Loading D" />}>
614+
<D />
615+
</Suspense>
616+
)}
617+
</SuspenseList>
618+
);
619+
}
620+
621+
// Mount
622+
await A.resolve();
623+
ReactNoop.render(<Foo step={0} />);
624+
expect(Scheduler).toFlushAndYield([
625+
'A',
626+
'Suspend! [B]',
627+
'Loading B',
628+
'Loading A',
629+
'Loading B',
630+
]);
631+
expect(ReactNoop).toMatchRenderedOutput(
632+
<>
633+
<span>Loading A</span>
634+
<span>Loading B</span>
635+
</>,
636+
);
637+
await B.resolve();
638+
expect(Scheduler).toFlushAndYield(['A', 'B']);
639+
expect(ReactNoop).toMatchRenderedOutput(
640+
<>
641+
<span>A</span>
642+
<span>B</span>
643+
</>,
644+
);
645+
646+
// Update
647+
await C.resolve();
648+
ReactNoop.render(<Foo step={1} />);
649+
expect(Scheduler).toFlushAndYield([
650+
'C',
651+
'Suspend! [D]',
652+
'Loading D',
653+
'Loading C',
654+
'Loading D',
655+
]);
656+
expect(ReactNoop).toMatchRenderedOutput(
657+
<>
658+
<span>Loading C</span>
659+
<span>Loading D</span>
660+
</>,
661+
);
662+
await D.resolve();
663+
expect(Scheduler).toFlushAndYield(['C', 'D']);
664+
expect(ReactNoop).toMatchRenderedOutput(
665+
<>
666+
<span>C</span>
667+
<span>D</span>
668+
</>,
669+
);
670+
});
671+
588672
it('avoided boundaries can be coordinate with SuspenseList', async () => {
589673
let A = createAsyncText('A');
590674
let B = createAsyncText('B');

0 commit comments

Comments
 (0)