Don't treat the last row in hidden as deleted if already mounted (#17206)

Already mounted rows that resuspend may be considered as part of a tail
if they're at the end. However, for purposes of the tail="..." option
they don't get deleted. We deal with that in cutOffTailIfNeeded.

However, if they're also the first to suspend in the "hidden" case, we have
a special case that deletes the actual rendered row. This needs to consider
if that row was already mounted or things go wrong.
This commit is contained in:
Sebastian Markbåge
2019-10-29 13:38:25 -07:00
committed by GitHub
parent 048879edad
commit 6cd365cac6
2 changed files with 92 additions and 3 deletions
+3 -2
View File
@@ -1098,10 +1098,11 @@ function completeWork(
// This might have been modified.
if (
renderState.tail === null &&
renderState.tailMode === 'hidden'
renderState.tailMode === 'hidden' &&
!renderedTail.alternate
) {
// We need to delete the row we just rendered.
// Reset the effect list to what it w as before we rendered this
// Reset the effect list to what it was before we rendered this
// child. The nested children have already appended themselves.
let lastEffect = (workInProgress.lastEffect =
renderState.lastEffect);
@@ -2023,7 +2023,7 @@ describe('ReactSuspenseList', () => {
);
});
it('eventually resolves two nested forwards suspense list with a hidden tail', async () => {
it('eventually resolves two nested forwards suspense lists with a hidden tail', async () => {
let B = createAsyncText('B');
function Foo({showB}) {
@@ -2135,4 +2135,92 @@ describe('ReactSuspenseList', () => {
</div>,
);
});
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 ? (
<Suspense fallback={<Text text="Loading B" />}>
<AsyncB />
</Suspense>
) : (
<Text text="Sync B" />
);
}
function Foo({updateList}) {
return (
<SuspenseList revealOrder="forwards" tail="hidden">
<Suspense key="A" fallback={<Text text="Loading A" />}>
<Text text="A" />
</Suspense>
<B key="B" updateList={updateList} />
</SuspenseList>
);
}
ReactNoop.render(<Foo />);
expect(Scheduler).toFlushAndYield(['A', 'Sync B']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span>A</span>
<span>Sync B</span>
</>,
);
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(
<>
<span>A</span>
<span>Loading B</span>
</>,
);
// Before we resolve we'll rerender the whole list.
// This should leave the tree intact.
ReactNoop.act(() => ReactNoop.render(<Foo updateList={true} />));
expect(Scheduler).toHaveYielded(['A', 'Suspend! [B]', 'Loading B']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span>A</span>
<span>Loading B</span>
</>,
);
await AsyncB.resolve();
expect(Scheduler).toFlushAndYield(['B']);
expect(ReactNoop).toMatchRenderedOutput(
<>
<span>A</span>
<span>B</span>
</>,
);
// This should be the same instance. I.e. it didn't
// remount.
expect(previousInst).toBe(setAsyncB);
});
});