diff --git a/packages/react-reconciler/src/ReactFiberRoot.js b/packages/react-reconciler/src/ReactFiberRoot.js
index 7ef93739039e0..facc33f2f4ba9 100644
--- a/packages/react-reconciler/src/ReactFiberRoot.js
+++ b/packages/react-reconciler/src/ReactFiberRoot.js
@@ -14,6 +14,7 @@ import type {TimeoutHandle, NoTimeout} from './ReactFiberHostConfig';
import type {Thenable} from './ReactFiberWorkLoop';
import type {Interaction} from 'scheduler/src/Tracing';
import type {SuspenseHydrationCallbacks} from './ReactFiberSuspenseComponent';
+import type {ReactPriorityLevel} from './SchedulerWithReactIntegration';
import {noTimeout} from './ReactFiberHostConfig';
import {createHostRootFiber} from './ReactFiber';
@@ -23,6 +24,7 @@ import {
enableSuspenseCallback,
} from 'shared/ReactFeatureFlags';
import {unstable_getThreadID} from 'scheduler/tracing';
+import {NoPriority} from './SchedulerWithReactIntegration';
// TODO: This should be lifted into the renderer.
export type Batch = {
@@ -69,12 +71,21 @@ type BaseFiberRootProperties = {|
callbackNode: *,
// Expiration of the callback associated with this root
callbackExpirationTime: ExpirationTime,
+ // Priority of the callback associated with this root
+ callbackPriority: ReactPriorityLevel,
// The earliest pending expiration time that exists in the tree
firstPendingTime: ExpirationTime,
// The latest pending expiration time that exists in the tree
lastPendingTime: ExpirationTime,
- // The time at which a suspended component pinged the root to render again
- pingTime: ExpirationTime,
+ // The earliest suspended expiration time that exists in the tree
+ firstSuspendedTime: ExpirationTime,
+ // The latest suspended expiration time that exists in the tree
+ lastSuspendedTime: ExpirationTime,
+ // The next known expiration time after the suspended range
+ nextKnownPendingLevel: ExpirationTime,
+ // The latest time at which a suspended component pinged the root to
+ // render again
+ lastPingedTime: ExpirationTime,
|};
// The following attributes are only used by interaction tracing builds.
@@ -117,10 +128,13 @@ function FiberRootNode(containerInfo, tag, hydrate) {
this.hydrate = hydrate;
this.firstBatch = null;
this.callbackNode = null;
- this.callbackExpirationTime = NoWork;
+ this.callbackPriority = NoPriority;
this.firstPendingTime = NoWork;
this.lastPendingTime = NoWork;
- this.pingTime = NoWork;
+ this.firstSuspendedTime = NoWork;
+ this.lastSuspendedTime = NoWork;
+ this.nextKnownPendingLevel = NoWork;
+ this.lastPingedTime = NoWork;
if (enableSchedulerTracing) {
this.interactionThreadID = unstable_getThreadID();
@@ -151,3 +165,98 @@ export function createFiberRoot(
return root;
}
+
+export function isRootSuspendedAtTime(
+ root: FiberRoot,
+ expirationTime: ExpirationTime,
+): boolean {
+ const firstSuspendedTime = root.firstSuspendedTime;
+ const lastSuspendedTime = root.lastSuspendedTime;
+ return (
+ firstSuspendedTime !== NoWork &&
+ (firstSuspendedTime >= expirationTime &&
+ lastSuspendedTime <= expirationTime)
+ );
+}
+
+export function markRootSuspendedAtTime(
+ root: FiberRoot,
+ expirationTime: ExpirationTime,
+): void {
+ const firstSuspendedTime = root.firstSuspendedTime;
+ const lastSuspendedTime = root.lastSuspendedTime;
+ if (firstSuspendedTime < expirationTime) {
+ root.firstSuspendedTime = expirationTime;
+ }
+ if (lastSuspendedTime > expirationTime || firstSuspendedTime === NoWork) {
+ root.lastSuspendedTime = expirationTime;
+ }
+
+ if (expirationTime <= root.lastPingedTime) {
+ root.lastPingedTime = NoWork;
+ }
+}
+
+export function markRootUpdatedAtTime(
+ root: FiberRoot,
+ expirationTime: ExpirationTime,
+): void {
+ // Update the range of pending times
+ const firstPendingTime = root.firstPendingTime;
+ if (expirationTime > firstPendingTime) {
+ root.firstPendingTime = expirationTime;
+ }
+ const lastPendingTime = root.lastPendingTime;
+ if (lastPendingTime === NoWork || expirationTime < lastPendingTime) {
+ root.lastPendingTime = expirationTime;
+ }
+
+ // Update the range of suspended times. Treat everything lower priority or
+ // equal to this update as unsuspended.
+ const firstSuspendedTime = root.firstSuspendedTime;
+ if (firstSuspendedTime !== NoWork) {
+ if (expirationTime >= firstSuspendedTime) {
+ // The entire suspended range is now unsuspended.
+ root.firstSuspendedTime = root.lastSuspendedTime = root.nextKnownPendingLevel = NoWork;
+ } else if (expirationTime >= root.lastSuspendedTime) {
+ root.lastSuspendedTime = expirationTime + 1;
+ }
+
+ // This is a pending level. Check if it's higher priority than the next
+ // known pending level.
+ if (expirationTime > root.nextKnownPendingLevel) {
+ root.nextKnownPendingLevel = expirationTime;
+ }
+ }
+}
+
+export function markRootFinishedAtTime(
+ root: FiberRoot,
+ finishedExpirationTime: ExpirationTime,
+ remainingExpirationTime: ExpirationTime,
+): void {
+ // Update the range of pending times
+ root.firstPendingTime = remainingExpirationTime;
+ if (remainingExpirationTime < root.lastPendingTime) {
+ // This usually means we've finished all the work, but it can also happen
+ // when something gets downprioritized during render, like a hidden tree.
+ root.lastPendingTime = remainingExpirationTime;
+ }
+
+ // Update the range of suspended times. Treat everything higher priority or
+ // equal to this update as unsuspended.
+ if (finishedExpirationTime <= root.lastSuspendedTime) {
+ // The entire suspended range is now unsuspended.
+ root.firstSuspendedTime = root.lastSuspendedTime = root.nextKnownPendingLevel = NoWork;
+ } else if (finishedExpirationTime <= root.firstSuspendedTime) {
+ // Part of the suspended range is now unsuspended. Narrow the range to
+ // include everything between the unsuspended time (non-inclusive) and the
+ // last suspended time.
+ root.firstSuspendedTime = finishedExpirationTime - 1;
+ }
+
+ if (finishedExpirationTime <= root.lastPingedTime) {
+ // Clear the pinged time
+ root.lastPingedTime = NoWork;
+ }
+}
diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js
index 9d9dad28a41aa..8f2915291845f 100644
--- a/packages/react-reconciler/src/ReactFiberWorkLoop.js
+++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js
@@ -66,6 +66,12 @@ import {
} from './ReactFiberHostConfig';
import {createWorkInProgress, assignFiberPropertiesInDEV} from './ReactFiber';
+import {
+ isRootSuspendedAtTime,
+ markRootSuspendedAtTime,
+ markRootFinishedAtTime,
+ markRootUpdatedAtTime,
+} from './ReactFiberRoot';
import {
NoMode,
StrictMode,
@@ -377,8 +383,6 @@ export function scheduleUpdateOnFiber(
return;
}
- root.pingTime = NoWork;
-
checkForInterruption(fiber, expirationTime);
recordScheduleUpdate();
@@ -404,7 +408,8 @@ export function scheduleUpdateOnFiber(
callback = callback(true);
}
} else {
- scheduleCallbackForRoot(root, ImmediatePriority, Sync);
+ ensureRootIsScheduled(root);
+ schedulePendingInteractions(root, expirationTime);
if (executionContext === NoContext) {
// Flush the synchronous work now, wnless we're already working or inside
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
@@ -415,7 +420,8 @@ export function scheduleUpdateOnFiber(
}
}
} else {
- scheduleCallbackForRoot(root, priorityLevel, expirationTime);
+ ensureRootIsScheduled(root);
+ schedulePendingInteractions(root, expirationTime);
}
if (
@@ -483,20 +489,36 @@ function markUpdateTimeFromFiberToRoot(fiber, expirationTime) {
}
if (root !== null) {
- // Update the first and last pending expiration times in this root
- const firstPendingTime = root.firstPendingTime;
- if (expirationTime > firstPendingTime) {
- root.firstPendingTime = expirationTime;
- }
- const lastPendingTime = root.lastPendingTime;
- if (lastPendingTime === NoWork || expirationTime < lastPendingTime) {
- root.lastPendingTime = expirationTime;
- }
+ markRootUpdatedAtTime(root, expirationTime);
}
return root;
}
+function getNextRootExpirationTimeToWorkOn(root: FiberRoot): ExpirationTime {
+ // Determines the next expiration time that the root should render, taking
+ // into account levels that may be suspended, or levels that may have
+ // received a ping.
+ //
+ // "Pending" refers to any update that hasn't committed yet, including if it
+ // suspended. The "suspended" range is therefore a subset.
+
+ const firstPendingTime = root.firstPendingTime;
+ if (!isRootSuspendedAtTime(root, firstPendingTime)) {
+ // The highest priority pending time is not suspended. Let's work on that.
+ return firstPendingTime;
+ }
+
+ // If the first pending time is suspended, check if there's a lower priority
+ // pending level that we know about. Or check if we received a ping. Work
+ // on whichever is higher priority.
+ const lastPingedTime = root.lastPingedTime;
+ const nextKnownPendingLevel = root.nextKnownPendingLevel;
+ return lastPingedTime > nextKnownPendingLevel
+ ? lastPingedTime
+ : nextKnownPendingLevel;
+}
+
// Use this function, along with runRootCallback, to ensure that only a single
// callback per root is scheduled. It's still possible to call renderRoot
// directly, but scheduling via this function helps avoid excessive callbacks.
@@ -505,53 +527,79 @@ function markUpdateTimeFromFiberToRoot(fiber, expirationTime) {
// should cancel the previous one. It also relies on commitRoot scheduling a
// callback to render the next level, because that means we don't need a
// separate callback per expiration time.
-function scheduleCallbackForRoot(
- root: FiberRoot,
- priorityLevel: ReactPriorityLevel,
- expirationTime: ExpirationTime,
-) {
- const existingCallbackExpirationTime = root.callbackExpirationTime;
- if (existingCallbackExpirationTime < expirationTime) {
- // New callback has higher priority than the existing one.
- const existingCallbackNode = root.callbackNode;
- if (existingCallbackNode !== null) {
- cancelCallback(existingCallbackNode);
- }
- root.callbackExpirationTime = expirationTime;
-
- if (expirationTime === Sync) {
- // Sync React callbacks are scheduled on a special internal queue
- root.callbackNode = scheduleSyncCallback(
- runRootCallback.bind(
- null,
- root,
- renderRoot.bind(null, root, expirationTime),
- ),
- );
- } else {
- let options = null;
- if (
- !disableSchedulerTimeoutBasedOnReactExpirationTime &&
- expirationTime !== Never
- ) {
- let timeout = expirationTimeToMs(expirationTime) - now();
- options = {timeout};
- }
+function ensureRootIsScheduled(root: FiberRoot) {
+ const expirationTime = getNextRootExpirationTimeToWorkOn(root);
+ if (expirationTime === NoWork) {
+ // Nothing to work on.
+ return;
+ }
- root.callbackNode = scheduleCallback(
- priorityLevel,
- runRootCallback.bind(
- null,
- root,
- renderRoot.bind(null, root, expirationTime),
- ),
- options,
- );
+ // TODO: If this is an update, we already read the current time. Pass the
+ // time as an argument.
+ const currentTime = requestCurrentTime();
+ const priorityLevel = inferPriorityFromExpirationTime(
+ currentTime,
+ expirationTime,
+ );
+
+ // If there's an existing render task, confirm it has the correct priority and
+ // expiration time. Otherwise, we'll cancel it and schedule a new one.
+ const existingCallbackNode = root.callbackNode;
+ if (existingCallbackNode !== null) {
+ const existingCallbackPriority = root.callbackPriority;
+ const existingCallbackExpirationTime = root.callbackExpirationTime;
+ if (
+ // Callback must have the exact same expiration time.
+ existingCallbackExpirationTime === expirationTime &&
+ // Callback must have greater or equal priority.
+ existingCallbackPriority >= priorityLevel
+ ) {
+ // Existing callback is sufficient.
+ return;
}
+ // Need to schedule a new task.
+ // TODO: Instead of scheduling a new task, we should be able to change the
+ // priority of the existing one.
+ cancelCallback(existingCallbackNode);
}
- // Associate the current interactions with this new root+priority.
- schedulePendingInteractions(root, expirationTime);
+ root.callbackExpirationTime = expirationTime;
+ root.callbackPriority = priorityLevel;
+
+ let callbackNode;
+ if (expirationTime === Sync) {
+ // Sync React callbacks are scheduled on a special internal queue
+ callbackNode = scheduleSyncCallback(
+ runRootCallback.bind(
+ null,
+ root,
+ renderRoot.bind(null, root, expirationTime),
+ ),
+ );
+ } else if (disableSchedulerTimeoutBasedOnReactExpirationTime) {
+ callbackNode = scheduleCallback(
+ priorityLevel,
+ runRootCallback.bind(
+ null,
+ root,
+ renderRoot.bind(null, root, expirationTime),
+ ),
+ );
+ } else {
+ callbackNode = scheduleCallback(
+ priorityLevel,
+ runRootCallback.bind(
+ null,
+ root,
+ renderRoot.bind(null, root, expirationTime),
+ ),
+ // Compute a task timeout based on the expiration time. This also affects
+ // ordering because tasks are processed in timeout order.
+ {timeout: expirationTimeToMs(expirationTime) - now()},
+ );
+ }
+
+ root.callbackNode = callbackNode;
}
function runRootCallback(root, callback, isSync) {
@@ -572,6 +620,7 @@ function runRootCallback(root, callback, isSync) {
if (continuation === null && prevCallbackNode === root.callbackNode) {
root.callbackNode = null;
root.callbackExpirationTime = NoWork;
+ root.callbackPriority = NoPriority;
}
}
}
@@ -807,18 +856,12 @@ function renderRoot(
'Should not already be working.',
);
- if (root.firstPendingTime < expirationTime) {
- // If there's no work left at this expiration time, exit immediately. This
- // happens when multiple callbacks are scheduled for a single root, but an
- // earlier callback flushes the work of a later one.
- return null;
- }
-
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);
+ commitRoot(root);
+ return null;
}
flushPassiveEffects();
@@ -828,26 +871,6 @@ function renderRoot(
if (root !== workInProgressRoot || expirationTime !== renderExpirationTime) {
prepareFreshStack(root, expirationTime);
startWorkOnPendingInteractions(root, expirationTime);
- } else if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
- // We could've received an update at a lower priority while we yielded.
- // We're suspended in a delayed state. Once we complete this render we're
- // just going to try to recover at the last pending time anyway so we might
- // as well start doing that eagerly.
- // Ideally we should be able to do this even for retries but we don't yet
- // know if we're going to process an update which wants to commit earlier,
- // and this path happens very early so it would happen too often. Instead,
- // for that case, we'll wait until we complete.
- if (workInProgressRootHasPendingPing) {
- // We have a ping at this expiration. Let's restart to see if we get unblocked.
- prepareFreshStack(root, expirationTime);
- } else {
- const lastPendingTime = root.lastPendingTime;
- if (lastPendingTime < expirationTime) {
- // There's lower priority work. It might be unsuspended. Try rendering
- // at that level immediately, while preserving the position in the queue.
- return renderRoot.bind(null, root, lastPendingTime);
- }
- }
}
// If we have a work-in-progress fiber, it means there's still work to do
@@ -918,6 +941,7 @@ function renderRoot(
// boundary.
prepareFreshStack(root, expirationTime);
executionContext = prevExecutionContext;
+ markRootSuspendedAtTime(root, expirationTime);
throw thrownValue;
}
@@ -958,7 +982,8 @@ function renderRoot(
// something suspended, wait to commit it after a timeout.
stopFinishedWorkLoopTimer();
- root.finishedWork = root.current.alternate;
+ const finishedWork: Fiber = ((root.finishedWork =
+ root.current.alternate): any);
root.finishedExpirationTime = expirationTime;
const isLocked = resolveLocksOnRoot(root, expirationTime);
@@ -1002,6 +1027,11 @@ function renderRoot(
return commitRoot.bind(null, root);
}
case RootSuspended: {
+ markRootSuspendedAtTime(root, expirationTime);
+ const lastSuspendedTime = root.lastSuspendedTime;
+ if (expirationTime === lastSuspendedTime) {
+ root.nextKnownPendingLevel = getRemainingExpirationTime(finishedWork);
+ }
flushSuspensePriorityWarningInDEV();
// We have an acceptable loading state. We need to figure out if we should
@@ -1038,11 +1068,20 @@ function renderRoot(
prepareFreshStack(root, expirationTime);
return renderRoot.bind(null, root, expirationTime);
}
- const lastPendingTime = root.lastPendingTime;
- if (lastPendingTime < expirationTime) {
+
+ const nextKnownPendingLevel = root.nextKnownPendingLevel;
+ if (nextKnownPendingLevel !== NoWork) {
// There's lower priority work. It might be unsuspended. Try rendering
// at that level.
- return renderRoot.bind(null, root, lastPendingTime);
+ return renderRoot.bind(null, root, nextKnownPendingLevel);
+ }
+ if (
+ lastSuspendedTime !== NoWork &&
+ lastSuspendedTime !== expirationTime
+ ) {
+ // We should prefer to render the fallback of at the last
+ // suspended level.
+ return renderRoot.bind(null, root, lastSuspendedTime);
}
// The render is suspended, it hasn't timed out, and there's no lower
// priority work to do. Instead of committing the fallback
@@ -1058,6 +1097,11 @@ function renderRoot(
return commitRoot.bind(null, root);
}
case RootSuspendedWithDelay: {
+ markRootSuspendedAtTime(root, expirationTime);
+ const lastSuspendedTime = root.lastSuspendedTime;
+ if (expirationTime === lastSuspendedTime) {
+ root.nextKnownPendingLevel = getRemainingExpirationTime(finishedWork);
+ }
flushSuspensePriorityWarningInDEV();
if (
@@ -1077,11 +1121,20 @@ function renderRoot(
prepareFreshStack(root, expirationTime);
return renderRoot.bind(null, root, expirationTime);
}
- const lastPendingTime = root.lastPendingTime;
- if (lastPendingTime < expirationTime) {
+
+ const nextKnownPendingLevel = root.nextKnownPendingLevel;
+ if (nextKnownPendingLevel !== NoWork) {
// There's lower priority work. It might be unsuspended. Try rendering
- // at that level immediately.
- return renderRoot.bind(null, root, lastPendingTime);
+ // at that level.
+ return renderRoot.bind(null, root, nextKnownPendingLevel);
+ }
+ if (
+ lastSuspendedTime !== NoWork &&
+ lastSuspendedTime !== expirationTime
+ ) {
+ // We should prefer to render the fallback of at the last
+ // suspended level.
+ return renderRoot.bind(null, root, lastSuspendedTime);
}
let msUntilTimeout;
@@ -1425,6 +1478,14 @@ function completeUnitOfWork(unitOfWork: Fiber): Fiber | null {
return null;
}
+function getRemainingExpirationTime(fiber: Fiber) {
+ const updateExpirationTime = fiber.expirationTime;
+ const childExpirationTime = fiber.childExpirationTime;
+ return updateExpirationTime > childExpirationTime
+ ? updateExpirationTime
+ : childExpirationTime;
+}
+
function resetChildExpirationTime(completedWork: Fiber) {
if (
renderExpirationTime !== Never &&
@@ -1535,23 +1596,21 @@ function commitRootImpl(root, renderPriorityLevel) {
// So we can clear these now to allow a new callback to be scheduled.
root.callbackNode = null;
root.callbackExpirationTime = NoWork;
+ root.callbackPriority = NoPriority;
+ root.nextKnownPendingLevel = NoWork;
startCommitTimer();
// Update the first and last pending times on this root. The new first
// pending time is whatever is left on the root fiber.
- const updateExpirationTimeBeforeCommit = finishedWork.expirationTime;
- const childExpirationTimeBeforeCommit = finishedWork.childExpirationTime;
- const firstPendingTimeBeforeCommit =
- childExpirationTimeBeforeCommit > updateExpirationTimeBeforeCommit
- ? childExpirationTimeBeforeCommit
- : updateExpirationTimeBeforeCommit;
- root.firstPendingTime = firstPendingTimeBeforeCommit;
- if (firstPendingTimeBeforeCommit < root.lastPendingTime) {
- // This usually means we've finished all the work, but it can also happen
- // when something gets downprioritized during render, like a hidden tree.
- root.lastPendingTime = firstPendingTimeBeforeCommit;
- }
+ const remainingExpirationTimeBeforeCommit = getRemainingExpirationTime(
+ finishedWork,
+ );
+ markRootFinishedAtTime(
+ root,
+ expirationTime,
+ remainingExpirationTimeBeforeCommit,
+ );
if (root === workInProgressRoot) {
// We can reset these now that they are finished.
@@ -1753,12 +1812,6 @@ function commitRootImpl(root, renderPriorityLevel) {
// Check if there's remaining work on this root
const remainingExpirationTime = root.firstPendingTime;
if (remainingExpirationTime !== NoWork) {
- const currentTime = requestCurrentTime();
- const priorityLevel = inferPriorityFromExpirationTime(
- currentTime,
- remainingExpirationTime,
- );
-
if (enableSchedulerTracing) {
if (spawnedWorkDuringRender !== null) {
const expirationTimes = spawnedWorkDuringRender;
@@ -1772,8 +1825,8 @@ function commitRootImpl(root, renderPriorityLevel) {
}
}
}
-
- scheduleCallbackForRoot(root, priorityLevel, remainingExpirationTime);
+ ensureRootIsScheduled(root);
+ schedulePendingInteractions(root, expirationTime);
} else {
// If there's no remaining work, we can clear the set of already failed
// error boundaries.
@@ -2061,7 +2114,8 @@ function captureCommitPhaseErrorOnRoot(
enqueueUpdate(rootFiber, update);
const root = markUpdateTimeFromFiberToRoot(rootFiber, Sync);
if (root !== null) {
- scheduleCallbackForRoot(root, ImmediatePriority, Sync);
+ ensureRootIsScheduled(root);
+ schedulePendingInteractions(root, Sync);
}
}
@@ -2096,7 +2150,8 @@ export function captureCommitPhaseError(sourceFiber: Fiber, error: mixed) {
enqueueUpdate(fiber, update);
const root = markUpdateTimeFromFiberToRoot(fiber, Sync);
if (root !== null) {
- scheduleCallbackForRoot(root, ImmediatePriority, Sync);
+ ensureRootIsScheduled(root);
+ schedulePendingInteractions(root, Sync);
}
return;
}
@@ -2148,20 +2203,19 @@ export function pingSuspendedRoot(
return;
}
- const lastPendingTime = root.lastPendingTime;
- if (lastPendingTime < suspendedTime) {
+ if (!isRootSuspendedAtTime(root, suspendedTime)) {
// The root is no longer suspended at this time.
return;
}
- const pingTime = root.pingTime;
- if (pingTime !== NoWork && pingTime < suspendedTime) {
+ const lastPingedTime = root.lastPingedTime;
+ if (lastPingedTime !== NoWork && lastPingedTime < suspendedTime) {
// There's already a lower priority ping scheduled.
return;
}
// Mark the time at which this ping was scheduled.
- root.pingTime = suspendedTime;
+ root.lastPingedTime = suspendedTime;
if (root.finishedExpirationTime === suspendedTime) {
// If there's a pending fallback waiting to commit, throw it away.
@@ -2169,12 +2223,8 @@ export function pingSuspendedRoot(
root.finishedWork = null;
}
- const currentTime = requestCurrentTime();
- const priorityLevel = inferPriorityFromExpirationTime(
- currentTime,
- suspendedTime,
- );
- scheduleCallbackForRoot(root, priorityLevel, suspendedTime);
+ ensureRootIsScheduled(root);
+ schedulePendingInteractions(root, suspendedTime);
}
function retryTimedOutBoundary(
@@ -2185,9 +2235,9 @@ function retryTimedOutBoundary(
// previously was rendered in its fallback state. One of the promises that
// suspended it has resolved, which means at least part of the tree was
// likely unblocked. Try rendering again, at a new expiration time.
- const currentTime = requestCurrentTime();
if (retryTime === Never) {
const suspenseConfig = null; // Retries don't carry over the already committed update.
+ const currentTime = requestCurrentTime();
retryTime = computeExpirationForFiber(
currentTime,
boundaryFiber,
@@ -2195,10 +2245,10 @@ function retryTimedOutBoundary(
);
}
// TODO: Special case idle priority?
- const priorityLevel = inferPriorityFromExpirationTime(currentTime, retryTime);
const root = markUpdateTimeFromFiberToRoot(boundaryFiber, retryTime);
if (root !== null) {
- scheduleCallbackForRoot(root, priorityLevel, retryTime);
+ ensureRootIsScheduled(root);
+ schedulePendingInteractions(root, retryTime);
}
}
diff --git a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js
index 65e672d87dceb..60dfcb0a55983 100644
--- a/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js
+++ b/packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js
@@ -518,6 +518,71 @@ describe('ReactSuspenseWithNoopRenderer', () => {
expect(ReactNoop.getChildren()).toEqual([span('(empty)')]);
});
+ it('tries each subsequent level after suspending', async () => {
+ const root = ReactNoop.createRoot();
+
+ function App({step, shouldSuspend}) {
+ return (
+
+
+ {shouldSuspend ? (
+
+ ) : (
+
+ )}
+
+ );
+ }
+
+ function interrupt() {
+ // React has a heuristic to batch all updates that occur within the same
+ // event. This is a trick to circumvent that heuristic.
+ ReactNoop.flushSync(() => {
+ ReactNoop.renderToRootWithID(null, 'other-root');
+ });
+ }
+
+ // Mount the Suspense boundary without suspending, so that the subsequent
+ // updates suspend with a delay.
+ await ReactNoop.act(async () => {
+ root.render();
+ });
+ await advanceTimers(1000);
+ expect(Scheduler).toHaveYielded(['Sibling', 'Step 0']);
+
+ // Schedule an update at several distinct expiration times
+ await ReactNoop.act(async () => {
+ root.render();
+ Scheduler.unstable_advanceTime(1000);
+ expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
+ interrupt();
+
+ root.render();
+ Scheduler.unstable_advanceTime(1000);
+ expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
+ interrupt();
+
+ root.render();
+ Scheduler.unstable_advanceTime(1000);
+ expect(Scheduler).toFlushAndYieldThrough(['Sibling']);
+ interrupt();
+
+ root.render();
+ });
+
+ // Should suspend at each distinct level
+ expect(Scheduler).toHaveYielded([
+ 'Sibling',
+ 'Suspend! [Step 1]',
+ 'Sibling',
+ 'Suspend! [Step 2]',
+ 'Sibling',
+ 'Suspend! [Step 3]',
+ 'Sibling',
+ 'Step 4',
+ ]);
+ });
+
it('forces an expiration after an update times out', async () => {
ReactNoop.render(