Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion packages/react-reconciler/src/ReactFiberWorkLoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,9 @@ export function computeExpirationForFiber(

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2294,4 +2294,51 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(ReactNoop.getChildren()).toEqual([span('D')]);
});
});

it("suspended commit remains suspended even if there's another update at same expiration", async () => {
// Regression test
function App({text}) {
return (
<Suspense fallback="Outer fallback">
<AsyncText ms={2000} text={text} />
</Suspense>
);
}

const root = ReactNoop.createRoot();
await ReactNoop.act(async () => {
root.render(<App text="Initial" />);
});

// Resolve initial render
await ReactNoop.act(async () => {
Scheduler.advanceTime(2000);
await advanceTimers(2000);
});
expect(Scheduler).toHaveYielded([
'Suspend! [Initial]',
'Promise resolved [Initial]',
'Initial',
]);
expect(root).toMatchRenderedOutput(<span prop="Initial" />);

// Suspend B. Since showing a fallback would hide content that's already
// visible, it should suspend for a bit without committing.
await ReactNoop.act(async () => {
root.render(<App text="First update" />);

expect(Scheduler).toFlushAndYield(['Suspend! [First update]']);
// Should not display a fallback
expect(root).toMatchRenderedOutput(<span prop="Initial" />);
});

// Suspend A. This should also suspend for a JND.
await ReactNoop.act(async () => {
root.render(<App text="Second update" />);

expect(Scheduler).toFlushAndYield(['Suspend! [Second update]']);
// Should not display a fallback
expect(root).toMatchRenderedOutput(<span prop="Initial" />);
});
});
});