Skip to content

Commit

Permalink
Don't treat the last row in hidden as deleted if already mounted (#17206
Browse files Browse the repository at this point in the history
)

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.
  • Loading branch information
sebmarkbage authored Oct 29, 2019
1 parent 048879e commit 6cd365c
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 3 deletions.
5 changes: 3 additions & 2 deletions packages/react-reconciler/src/ReactFiberCompleteWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}) {
Expand Down Expand Up @@ -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);
});
});

0 comments on commit 6cd365c

Please sign in to comment.