Skip to content

Commit 788da69

Browse files
authored
[Suspense] Fix bad loading state not being delayed (#15891)
Fixes a bug where a bad loading state is initially suspended, but a subsequent update with the same expiration time causes it to commit immediately.
1 parent 353e0ee commit 788da69

File tree

2 files changed

+53
-1
lines changed

2 files changed

+53
-1
lines changed

‎packages/react-reconciler/src/ReactFiberWorkLoop.js‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,9 @@ export function computeExpirationForFiber(
323323

324324
// If we're in the middle of rendering a tree, do not update at the same
325325
// expiration time that is already rendering.
326+
// TODO: We shouldn't have to do this if the update is on a different root.
327+
// Refactor computeExpirationForFiber + scheduleUpdate so we have access to
328+
// the root when we check for this condition.
326329
if (workInProgressRoot !== null && expirationTime === renderExpirationTime) {
327330
// This is a trick to move this update into a separate batch
328331
expirationTime -= 1;
@@ -806,8 +809,10 @@ function renderRoot(
806809
return null;
807810
}
808811

809-
if (root.finishedExpirationTime === expirationTime) {
812+
if (isSync && root.finishedExpirationTime === expirationTime) {
810813
// There's already a pending commit at this expiration time.
814+
// TODO: This is poorly factored. This case only exists for the
815+
// batch.commit() API.
811816
return commitRoot.bind(null, root);
812817
}
813818

‎packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2294,4 +2294,51 @@ describe('ReactSuspenseWithNoopRenderer', () => {
22942294
expect(ReactNoop.getChildren()).toEqual([span('D')]);
22952295
});
22962296
});
2297+
2298+
it("suspended commit remains suspended even if there's another update at same expiration", async () => {
2299+
// Regression test
2300+
function App({text}) {
2301+
return (
2302+
<Suspense fallback="Outer fallback">
2303+
<AsyncText ms={2000} text={text} />
2304+
</Suspense>
2305+
);
2306+
}
2307+
2308+
const root = ReactNoop.createRoot();
2309+
await ReactNoop.act(async () => {
2310+
root.render(<App text="Initial" />);
2311+
});
2312+
2313+
// Resolve initial render
2314+
await ReactNoop.act(async () => {
2315+
Scheduler.advanceTime(2000);
2316+
await advanceTimers(2000);
2317+
});
2318+
expect(Scheduler).toHaveYielded([
2319+
'Suspend! [Initial]',
2320+
'Promise resolved [Initial]',
2321+
'Initial',
2322+
]);
2323+
expect(root).toMatchRenderedOutput(<span prop="Initial" />);
2324+
2325+
// Suspend B. Since showing a fallback would hide content that's already
2326+
// visible, it should suspend for a bit without committing.
2327+
await ReactNoop.act(async () => {
2328+
root.render(<App text="First update" />);
2329+
2330+
expect(Scheduler).toFlushAndYield(['Suspend! [First update]']);
2331+
// Should not display a fallback
2332+
expect(root).toMatchRenderedOutput(<span prop="Initial" />);
2333+
});
2334+
2335+
// Suspend A. This should also suspend for a JND.
2336+
await ReactNoop.act(async () => {
2337+
root.render(<App text="Second update" />);
2338+
2339+
expect(Scheduler).toFlushAndYield(['Suspend! [Second update]']);
2340+
// Should not display a fallback
2341+
expect(root).toMatchRenderedOutput(<span prop="Initial" />);
2342+
});
2343+
});
22972344
});

0 commit comments

Comments
 (0)