Skip to content

Commit

Permalink
Move suspended render logic to ensureRootIsScheduled
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
acdlite committed Mar 6, 2023
1 parent 1528c5c commit bb38ccd
Show file tree
Hide file tree
Showing 2 changed files with 68 additions and 18 deletions.
45 changes: 27 additions & 18 deletions packages/react-reconciler/src/ReactFiberWorkLoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ const SuspendedOnError: SuspendedReason = 1;
const SuspendedOnData: SuspendedReason = 2;
const SuspendedOnImmediate: SuspendedReason = 3;
const SuspendedOnDeprecatedThrowPromise: SuspendedReason = 4;
const SuspendedAndReadyToUnwind: SuspendedReason = 5;
const SuspendedAndReadyToContinue: SuspendedReason = 5;
const SuspendedOnHydration: SuspendedReason = 6;

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

// If this root is currently suspended and waiting for data to resolve, don't
// schedule a task to render it. We'll either wait for a ping, or wait to
// receive an update.
if (
workInProgressSuspendedReason === SuspendedOnData &&
workInProgressRoot === root
) {
root.callbackPriority = NoLane;
root.callbackNode = null;
return;
}

// We use the highest priority lane to represent the priority of the callback.
const newCallbackPriority = getHighestPriorityLane(nextLanes);

Expand Down Expand Up @@ -1153,20 +1165,6 @@ function performConcurrentWorkOnRoot(
if (root.callbackNode === originalCallbackNode) {
// The task node scheduled for this root is the same one that's
// currently executed. Need to return a continuation.
if (
workInProgressSuspendedReason === SuspendedOnData &&
workInProgressRoot === root
) {
// Special case: The work loop is currently suspended and waiting for
// data to resolve. Unschedule the current task.
//
// TODO: The factoring is a little weird. Arguably this should be checked
// in ensureRootIsScheduled instead. I went back and forth, not totally
// sure yet.
root.callbackPriority = NoLane;
root.callbackNode = null;
return null;
}
return performConcurrentWorkOnRoot.bind(null, root);
}
return null;
Expand Down Expand Up @@ -1858,7 +1856,7 @@ function handleThrow(root: FiberRoot, thrownValue: any): void {
case SuspendedOnData:
case SuspendedOnImmediate:
case SuspendedOnDeprecatedThrowPromise:
case SuspendedAndReadyToUnwind: {
case SuspendedAndReadyToContinue: {
const wakeable: Wakeable = (thrownValue: any);
markComponentSuspended(
erroredWork,
Expand Down Expand Up @@ -2216,6 +2214,17 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
// `status` field, but if the promise already has a status, we won't
// have added a listener until right here.
const onResolution = () => {
// Check if the root is still suspended on this promise.
if (
workInProgressSuspendedReason === SuspendedOnData &&
workInProgressRoot === root
) {
// Mark the root as ready to continue rendering.
workInProgressSuspendedReason = SuspendedAndReadyToContinue;
}
// Ensure the root is scheduled. We should do this even if we're
// currently working on a different root, so that we resume
// rendering later.
ensureRootIsScheduled(root, now());
};
thenable.then(onResolution, onResolution);
Expand All @@ -2225,10 +2234,10 @@ function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
// If this fiber just suspended, it's possible the data is already
// cached. Yield to the main thread to give it a chance to ping. If
// it does, we can retry immediately without unwinding the stack.
workInProgressSuspendedReason = SuspendedAndReadyToUnwind;
workInProgressSuspendedReason = SuspendedAndReadyToContinue;
break outer;
}
case SuspendedAndReadyToUnwind: {
case SuspendedAndReadyToContinue: {
const thenable: Thenable<mixed> = (thrownValue: any);
if (isThenableResolved(thenable)) {
// The data resolved. Try rendering the component again.
Expand Down
41 changes: 41 additions & 0 deletions packages/react-reconciler/src/__tests__/ReactThenable-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,47 @@ describe('ReactThenable', () => {
assertLog(['Something different']);
});

// @gate enableUseHook
test('when waiting for data to resolve, an update on a different root does not cause work to be dropped', async () => {
const getCachedAsyncText = cache(getAsyncText);

function App() {
return <Text text={use(getCachedAsyncText('Hi'))} />;
}

const root1 = ReactNoop.createRoot();
await act(async () => {
root1.render(<Suspense fallback={<Text text="Loading..." />} />);
});

// Start a transition on one root. It will suspend.
await act(async () => {
startTransition(() => {
root1.render(
<Suspense fallback={<Text text="Loading..." />}>
<App />
</Suspense>,
);
});
});
assertLog(['Async text requested [Hi]']);

// While we're waiting for the first root's data to resolve, a second
// root renders.
const root2 = ReactNoop.createRoot();
await act(async () => {
root2.render('Do re mi');
});
expect(root2).toMatchRenderedOutput('Do re mi');

// Once the first root's data is ready, we should finish its transition.
await act(async () => {
await resolveTextRequests('Hi');
});
assertLog(['Hi']);
expect(root1).toMatchRenderedOutput('Hi');
});

// @gate enableUseHook
test('while suspended, hooks cannot be called (i.e. current dispatcher is unset correctly)', async () => {
function App() {
Expand Down

0 comments on commit bb38ccd

Please sign in to comment.