Skip to content

Commit 6e1756a

Browse files
authored
Move suspended render logic to ensureRootIsScheduled (#26328)
When the work loop is suspended, we shouldn't schedule a new render task until the promise has resolved. When I originally implemented this, I wasn't sure where to put this logic — `ensureRootIsScheduled` is the more natural place for it, but that's also a really hot path, so I chose to do it elsewhere, and left a TODO to reconsider later. Now it's later. I'm working on a refactor to move the `ensureRootIsScheduled` call to always happen in a microtask, so that if there are multiple updates/pings in a single event, they get batched into a single operation. Which means I can put the logic in that function where it belongs.
1 parent 1528c5c commit 6e1756a

File tree

2 files changed

+68
-18
lines changed

2 files changed

+68
-18
lines changed

packages/react-reconciler/src/ReactFiberWorkLoop.js

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ const SuspendedOnError: SuspendedReason = 1;
322322
const SuspendedOnData: SuspendedReason = 2;
323323
const SuspendedOnImmediate: SuspendedReason = 3;
324324
const SuspendedOnDeprecatedThrowPromise: SuspendedReason = 4;
325-
const SuspendedAndReadyToUnwind: SuspendedReason = 5;
325+
const SuspendedAndReadyToContinue: SuspendedReason = 5;
326326
const SuspendedOnHydration: SuspendedReason = 6;
327327

328328
// When this is true, the work-in-progress fiber just suspended (or errored) and
@@ -892,6 +892,18 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
892892
return;
893893
}
894894

895+
// If this root is currently suspended and waiting for data to resolve, don't
896+
// schedule a task to render it. We'll either wait for a ping, or wait to
897+
// receive an update.
898+
if (
899+
workInProgressSuspendedReason === SuspendedOnData &&
900+
workInProgressRoot === root
901+
) {
902+
root.callbackPriority = NoLane;
903+
root.callbackNode = null;
904+
return;
905+
}
906+
895907
// We use the highest priority lane to represent the priority of the callback.
896908
const newCallbackPriority = getHighestPriorityLane(nextLanes);
897909

@@ -1153,20 +1165,6 @@ function performConcurrentWorkOnRoot(
11531165
if (root.callbackNode === originalCallbackNode) {
11541166
// The task node scheduled for this root is the same one that's
11551167
// currently executed. Need to return a continuation.
1156-
if (
1157-
workInProgressSuspendedReason === SuspendedOnData &&
1158-
workInProgressRoot === root
1159-
) {
1160-
// Special case: The work loop is currently suspended and waiting for
1161-
// data to resolve. Unschedule the current task.
1162-
//
1163-
// TODO: The factoring is a little weird. Arguably this should be checked
1164-
// in ensureRootIsScheduled instead. I went back and forth, not totally
1165-
// sure yet.
1166-
root.callbackPriority = NoLane;
1167-
root.callbackNode = null;
1168-
return null;
1169-
}
11701168
return performConcurrentWorkOnRoot.bind(null, root);
11711169
}
11721170
return null;
@@ -1858,7 +1856,7 @@ function handleThrow(root: FiberRoot, thrownValue: any): void {
18581856
case SuspendedOnData:
18591857
case SuspendedOnImmediate:
18601858
case SuspendedOnDeprecatedThrowPromise:
1861-
case SuspendedAndReadyToUnwind: {
1859+
case SuspendedAndReadyToContinue: {
18621860
const wakeable: Wakeable = (thrownValue: any);
18631861
markComponentSuspended(
18641862
erroredWork,
@@ -2216,6 +2214,17 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
22162214
// `status` field, but if the promise already has a status, we won't
22172215
// have added a listener until right here.
22182216
const onResolution = () => {
2217+
// Check if the root is still suspended on this promise.
2218+
if (
2219+
workInProgressSuspendedReason === SuspendedOnData &&
2220+
workInProgressRoot === root
2221+
) {
2222+
// Mark the root as ready to continue rendering.
2223+
workInProgressSuspendedReason = SuspendedAndReadyToContinue;
2224+
}
2225+
// Ensure the root is scheduled. We should do this even if we're
2226+
// currently working on a different root, so that we resume
2227+
// rendering later.
22192228
ensureRootIsScheduled(root, now());
22202229
};
22212230
thenable.then(onResolution, onResolution);
@@ -2225,10 +2234,10 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
22252234
// If this fiber just suspended, it's possible the data is already
22262235
// cached. Yield to the main thread to give it a chance to ping. If
22272236
// it does, we can retry immediately without unwinding the stack.
2228-
workInProgressSuspendedReason = SuspendedAndReadyToUnwind;
2237+
workInProgressSuspendedReason = SuspendedAndReadyToContinue;
22292238
break outer;
22302239
}
2231-
case SuspendedAndReadyToUnwind: {
2240+
case SuspendedAndReadyToContinue: {
22322241
const thenable: Thenable<mixed> = (thrownValue: any);
22332242
if (isThenableResolved(thenable)) {
22342243
// The data resolved. Try rendering the component again.

packages/react-reconciler/src/__tests__/ReactThenable-test.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,47 @@ describe('ReactThenable', () => {
650650
assertLog(['Something different']);
651651
});
652652

653+
// @gate enableUseHook
654+
test('when waiting for data to resolve, an update on a different root does not cause work to be dropped', async () => {
655+
const getCachedAsyncText = cache(getAsyncText);
656+
657+
function App() {
658+
return <Text text={use(getCachedAsyncText('Hi'))} />;
659+
}
660+
661+
const root1 = ReactNoop.createRoot();
662+
await act(async () => {
663+
root1.render(<Suspense fallback={<Text text="Loading..." />} />);
664+
});
665+
666+
// Start a transition on one root. It will suspend.
667+
await act(async () => {
668+
startTransition(() => {
669+
root1.render(
670+
<Suspense fallback={<Text text="Loading..." />}>
671+
<App />
672+
</Suspense>,
673+
);
674+
});
675+
});
676+
assertLog(['Async text requested [Hi]']);
677+
678+
// While we're waiting for the first root's data to resolve, a second
679+
// root renders.
680+
const root2 = ReactNoop.createRoot();
681+
await act(async () => {
682+
root2.render('Do re mi');
683+
});
684+
expect(root2).toMatchRenderedOutput('Do re mi');
685+
686+
// Once the first root's data is ready, we should finish its transition.
687+
await act(async () => {
688+
await resolveTextRequests('Hi');
689+
});
690+
assertLog(['Hi']);
691+
expect(root1).toMatchRenderedOutput('Hi');
692+
});
693+
653694
// @gate enableUseHook
654695
test('while suspended, hooks cannot be called (i.e. current dispatcher is unset correctly)', async () => {
655696
function App() {

0 commit comments

Comments
 (0)