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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy