mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Decouple expiration times and transition timeouts (#17920)
We currently use the expiration time to represent the timeout of a transition. Since we intend to stop treating work priority as a timeline, we can no longer use this trick. In this commit, I've changed it to store the event time on the update object instead. Long term, we will store event time on the root as a map of transition -> event time. I'm only storing it on the update object as a temporary workaround to unblock the rest of the changes.
This commit is contained in:
@@ -196,7 +196,7 @@ const classComponentUpdater = {
|
||||
suspenseConfig,
|
||||
);
|
||||
|
||||
const update = createUpdate(expirationTime, suspenseConfig);
|
||||
const update = createUpdate(currentTime, expirationTime, suspenseConfig);
|
||||
update.payload = payload;
|
||||
if (callback !== undefined && callback !== null) {
|
||||
if (__DEV__) {
|
||||
@@ -218,7 +218,7 @@ const classComponentUpdater = {
|
||||
suspenseConfig,
|
||||
);
|
||||
|
||||
const update = createUpdate(expirationTime, suspenseConfig);
|
||||
const update = createUpdate(currentTime, expirationTime, suspenseConfig);
|
||||
update.tag = ReplaceState;
|
||||
update.payload = payload;
|
||||
|
||||
@@ -242,7 +242,7 @@ const classComponentUpdater = {
|
||||
suspenseConfig,
|
||||
);
|
||||
|
||||
const update = createUpdate(expirationTime, suspenseConfig);
|
||||
const update = createUpdate(currentTime, expirationTime, suspenseConfig);
|
||||
update.tag = ForceUpdate;
|
||||
|
||||
if (callback !== undefined && callback !== null) {
|
||||
|
||||
@@ -35,6 +35,8 @@ export const Idle = 2;
|
||||
// Continuous Hydration is slightly higher than Idle and is used to increase
|
||||
// priority of hover targets.
|
||||
export const ContinuousHydration = 3;
|
||||
export const LongTransition = 49999;
|
||||
export const ShortTransition = 99999;
|
||||
export const Sync = MAX_SIGNED_31_BIT_INT;
|
||||
export const Batched = Sync - 1;
|
||||
|
||||
@@ -84,16 +86,13 @@ export function computeAsyncExpiration(
|
||||
);
|
||||
}
|
||||
|
||||
export function computeSuspenseExpiration(
|
||||
export function computeSuspenseTimeout(
|
||||
currentTime: ExpirationTime,
|
||||
timeoutMs: number,
|
||||
): ExpirationTime {
|
||||
// TODO: Should we warn if timeoutMs is lower than the normal pri expiration time?
|
||||
return computeExpirationBucket(
|
||||
currentTime,
|
||||
timeoutMs,
|
||||
LOW_PRIORITY_BATCH_SIZE,
|
||||
);
|
||||
const currentTimeMs = expirationTimeToMs(currentTime);
|
||||
const deadlineMs = currentTimeMs + timeoutMs;
|
||||
return msToExpirationTime(deadlineMs);
|
||||
}
|
||||
|
||||
// We intentionally set a higher expiration time for interactive updates in
|
||||
|
||||
@@ -96,6 +96,9 @@ import {getIsRendering} from './ReactCurrentFiber';
|
||||
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
|
||||
|
||||
type Update<S, A> = {|
|
||||
// TODO: Temporary field. Will remove this by storing a map of
|
||||
// transition -> start time on the root.
|
||||
eventTime: ExpirationTime,
|
||||
expirationTime: ExpirationTime,
|
||||
suspenseConfig: null | SuspenseConfig,
|
||||
action: A,
|
||||
@@ -715,14 +718,17 @@ function updateReducer<S, I, A>(
|
||||
let newBaseQueueLast = null;
|
||||
let update = first;
|
||||
do {
|
||||
const suspenseConfig = update.suspenseConfig;
|
||||
const updateExpirationTime = update.expirationTime;
|
||||
const updateEventTime = update.eventTime;
|
||||
if (updateExpirationTime < renderExpirationTime) {
|
||||
// Priority is insufficient. Skip this update. If this is the first
|
||||
// skipped update, the previous update/state is the new base
|
||||
// update/state.
|
||||
const clone: Update<S, A> = {
|
||||
expirationTime: update.expirationTime,
|
||||
suspenseConfig: update.suspenseConfig,
|
||||
eventTime: updateEventTime,
|
||||
expirationTime: updateExpirationTime,
|
||||
suspenseConfig: suspenseConfig,
|
||||
action: update.action,
|
||||
eagerReducer: update.eagerReducer,
|
||||
eagerState: update.eagerState,
|
||||
@@ -744,6 +750,7 @@ function updateReducer<S, I, A>(
|
||||
|
||||
if (newBaseQueueLast !== null) {
|
||||
const clone: Update<S, A> = {
|
||||
eventTime: updateEventTime,
|
||||
expirationTime: Sync, // This update is going to be committed so we never want uncommit it.
|
||||
suspenseConfig: update.suspenseConfig,
|
||||
action: update.action,
|
||||
@@ -760,10 +767,7 @@ function updateReducer<S, I, A>(
|
||||
// TODO: We should skip this update if it was already committed but currently
|
||||
// we have no way of detecting the difference between a committed and suspended
|
||||
// update here.
|
||||
markRenderEventTimeAndConfig(
|
||||
updateExpirationTime,
|
||||
update.suspenseConfig,
|
||||
);
|
||||
markRenderEventTimeAndConfig(updateEventTime, suspenseConfig);
|
||||
|
||||
// Process this update.
|
||||
if (update.eagerReducer === reducer) {
|
||||
@@ -1651,6 +1655,7 @@ function dispatchAction<S, A>(
|
||||
);
|
||||
|
||||
const update: Update<S, A> = {
|
||||
eventTime: currentTime,
|
||||
expirationTime,
|
||||
suspenseConfig,
|
||||
action,
|
||||
|
||||
@@ -206,7 +206,7 @@ export function propagateContextChange(
|
||||
|
||||
if (fiber.tag === ClassComponent) {
|
||||
// Schedule a force update on the work-in-progress.
|
||||
const update = createUpdate(renderExpirationTime, null);
|
||||
const update = createUpdate(NoWork, renderExpirationTime, null);
|
||||
update.tag = ForceUpdate;
|
||||
// TODO: Because we don't have a work-in-progress, this will add the
|
||||
// update to the current fiber, too, which means it will persist even if
|
||||
|
||||
@@ -271,7 +271,7 @@ export function updateContainer(
|
||||
}
|
||||
}
|
||||
|
||||
const update = createUpdate(expirationTime, suspenseConfig);
|
||||
const update = createUpdate(currentTime, expirationTime, suspenseConfig);
|
||||
// Caution: React DevTools currently depends on this property
|
||||
// being called "element".
|
||||
update.payload = {element};
|
||||
|
||||
@@ -56,7 +56,7 @@ import {
|
||||
} from './ReactFiberWorkLoop.new';
|
||||
import {logCapturedError} from './ReactFiberErrorLogger';
|
||||
|
||||
import {Sync} from './ReactFiberExpirationTime.new';
|
||||
import {Sync, NoWork} from './ReactFiberExpirationTime.new';
|
||||
|
||||
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
|
||||
|
||||
@@ -65,7 +65,7 @@ function createRootErrorUpdate(
|
||||
errorInfo: CapturedValue<mixed>,
|
||||
expirationTime: ExpirationTime,
|
||||
): Update<mixed> {
|
||||
const update = createUpdate(expirationTime, null);
|
||||
const update = createUpdate(NoWork, expirationTime, null);
|
||||
// Unmount the root by rendering null.
|
||||
update.tag = CaptureUpdate;
|
||||
// Caution: React DevTools currently depends on this property
|
||||
@@ -84,7 +84,7 @@ function createClassErrorUpdate(
|
||||
errorInfo: CapturedValue<mixed>,
|
||||
expirationTime: ExpirationTime,
|
||||
): Update<mixed> {
|
||||
const update = createUpdate(expirationTime, null);
|
||||
const update = createUpdate(NoWork, expirationTime, null);
|
||||
update.tag = CaptureUpdate;
|
||||
const getDerivedStateFromError = fiber.type.getDerivedStateFromError;
|
||||
if (typeof getDerivedStateFromError === 'function') {
|
||||
@@ -260,7 +260,7 @@ function throwException(
|
||||
// When we try rendering again, we should not reuse the current fiber,
|
||||
// since it's known to be in an inconsistent state. Use a force update to
|
||||
// prevent a bail out.
|
||||
const update = createUpdate(Sync, null);
|
||||
const update = createUpdate(NoWork, Sync, null);
|
||||
update.tag = ForceUpdate;
|
||||
enqueueUpdate(sourceFiber, update);
|
||||
}
|
||||
|
||||
@@ -119,11 +119,13 @@ import {
|
||||
expirationTimeToMs,
|
||||
computeInteractiveExpiration,
|
||||
computeAsyncExpiration,
|
||||
computeSuspenseExpiration,
|
||||
inferPriorityFromExpirationTime,
|
||||
computeSuspenseTimeout,
|
||||
LOW_PRIORITY_EXPIRATION,
|
||||
inferPriorityFromExpirationTime,
|
||||
Batched,
|
||||
Idle,
|
||||
ShortTransition,
|
||||
LongTransition,
|
||||
} from './ReactFiberExpirationTime.new';
|
||||
import {beginWork as originalBeginWork} from './ReactFiberBeginWork.new';
|
||||
import {completeWork} from './ReactFiberCompleteWork.new';
|
||||
@@ -220,7 +222,7 @@ let workInProgressRootFatalError: mixed = null;
|
||||
// This is conceptually a time stamp but expressed in terms of an ExpirationTime
|
||||
// because we deal mostly with expiration times in the hot path, so this avoids
|
||||
// the conversion happening in the hot path.
|
||||
let workInProgressRootLatestProcessedExpirationTime: ExpirationTime = Sync;
|
||||
let workInProgressRootLatestProcessedEventTime: ExpirationTime = Sync;
|
||||
let workInProgressRootLatestSuspenseTimeout: ExpirationTime = Sync;
|
||||
let workInProgressRootCanSuspendUsingConfig: null | SuspenseConfig = null;
|
||||
// The work left over by components that were visited during this render. Only
|
||||
@@ -328,11 +330,16 @@ export function computeExpirationForFiber(
|
||||
|
||||
let expirationTime;
|
||||
if (suspenseConfig !== null) {
|
||||
// Compute an expiration time based on the Suspense timeout.
|
||||
expirationTime = computeSuspenseExpiration(
|
||||
currentTime,
|
||||
suspenseConfig.timeoutMs | 0 || LOW_PRIORITY_EXPIRATION,
|
||||
);
|
||||
// If there's a SuspenseConfig, choose an expiration time that's lower
|
||||
// priority than a normal concurrent update (regardless of the current
|
||||
// Scheduler priority.) Timeouts larger than 10 seconds move one level lower
|
||||
// than that.
|
||||
const timeoutMs = suspenseConfig.timeoutMs;
|
||||
expirationTime =
|
||||
// TODO: This will coerce numbers larger than 31 bits to 0.
|
||||
timeoutMs === undefined || (timeoutMs | 0) < 10000
|
||||
? ShortTransition
|
||||
: LongTransition;
|
||||
} else {
|
||||
// Compute an expiration time based on the Scheduler priority.
|
||||
switch (priorityLevel) {
|
||||
@@ -379,7 +386,7 @@ export function scheduleUpdateOnFiber(
|
||||
const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);
|
||||
if (root === null) {
|
||||
warnAboutUpdateOnUnmountedFiberInDEV(fiber);
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO: computeExpirationForFiber also reads the priority. Pass the
|
||||
@@ -728,7 +735,7 @@ function finishConcurrentRender(
|
||||
// have a new loading state ready. We want to ensure that we commit
|
||||
// that as soon as possible.
|
||||
const hasNotProcessedNewUpdates =
|
||||
workInProgressRootLatestProcessedExpirationTime === Sync;
|
||||
workInProgressRootLatestProcessedEventTime === Sync;
|
||||
if (
|
||||
hasNotProcessedNewUpdates &&
|
||||
// do not delay if we're inside an act() scope
|
||||
@@ -836,34 +843,19 @@ function finishConcurrentRender(
|
||||
// can use as the timeout.
|
||||
msUntilTimeout =
|
||||
expirationTimeToMs(workInProgressRootLatestSuspenseTimeout) - now();
|
||||
} else if (workInProgressRootLatestProcessedExpirationTime === Sync) {
|
||||
} else if (workInProgressRootLatestProcessedEventTime === Sync) {
|
||||
// This should never normally happen because only new updates
|
||||
// cause delayed states, so we should have processed something.
|
||||
// However, this could also happen in an offscreen tree.
|
||||
msUntilTimeout = 0;
|
||||
} else {
|
||||
// If we don't have a suspense config, we're going to use a
|
||||
// heuristic to determine how long we can suspend.
|
||||
const eventTimeMs: number = inferTimeFromExpirationTime(
|
||||
workInProgressRootLatestProcessedExpirationTime,
|
||||
// If we didn't process a suspense config, compute a JND based on
|
||||
// the amount of time elapsed since the most recent event time.
|
||||
const eventTimeMs = expirationTimeToMs(
|
||||
workInProgressRootLatestProcessedEventTime,
|
||||
);
|
||||
const currentTimeMs = now();
|
||||
const timeUntilExpirationMs =
|
||||
expirationTimeToMs(expirationTime) - currentTimeMs;
|
||||
let timeElapsed = currentTimeMs - eventTimeMs;
|
||||
if (timeElapsed < 0) {
|
||||
// We get this wrong some time since we estimate the time.
|
||||
timeElapsed = 0;
|
||||
}
|
||||
|
||||
msUntilTimeout = jnd(timeElapsed) - timeElapsed;
|
||||
|
||||
// Clamp the timeout to the expiration time. TODO: Once the
|
||||
// event time is exact instead of inferred from expiration time
|
||||
// we don't need this.
|
||||
if (timeUntilExpirationMs < msUntilTimeout) {
|
||||
msUntilTimeout = timeUntilExpirationMs;
|
||||
}
|
||||
const timeElapsedMs = now() - eventTimeMs;
|
||||
msUntilTimeout = jnd(timeElapsedMs) - timeElapsedMs;
|
||||
}
|
||||
|
||||
// Don't bother with a very short suspense time.
|
||||
@@ -891,7 +883,7 @@ function finishConcurrentRender(
|
||||
flushSuspenseFallbacksInTests &&
|
||||
IsThisRendererActing.current
|
||||
) &&
|
||||
workInProgressRootLatestProcessedExpirationTime !== Sync &&
|
||||
workInProgressRootLatestProcessedEventTime !== Sync &&
|
||||
workInProgressRootCanSuspendUsingConfig !== null
|
||||
) {
|
||||
// If we have exceeded the minimum loading delay, which probably
|
||||
@@ -899,7 +891,7 @@ function finishConcurrentRender(
|
||||
// a bit longer to ensure that the spinner is shown for
|
||||
// enough time.
|
||||
const msUntilTimeout = computeMsUntilSuspenseLoadingDelay(
|
||||
workInProgressRootLatestProcessedExpirationTime,
|
||||
workInProgressRootLatestProcessedEventTime,
|
||||
expirationTime,
|
||||
workInProgressRootCanSuspendUsingConfig,
|
||||
);
|
||||
@@ -1193,7 +1185,7 @@ function prepareFreshStack(root, expirationTime) {
|
||||
renderExpirationTime = expirationTime;
|
||||
workInProgressRootExitStatus = RootIncomplete;
|
||||
workInProgressRootFatalError = null;
|
||||
workInProgressRootLatestProcessedExpirationTime = Sync;
|
||||
workInProgressRootLatestProcessedEventTime = Sync;
|
||||
workInProgressRootLatestSuspenseTimeout = Sync;
|
||||
workInProgressRootCanSuspendUsingConfig = null;
|
||||
workInProgressRootNextUnprocessedUpdateTime = NoWork;
|
||||
@@ -1298,23 +1290,33 @@ export function markCommitTimeOfFallback() {
|
||||
}
|
||||
|
||||
export function markRenderEventTimeAndConfig(
|
||||
expirationTime: ExpirationTime,
|
||||
eventTime: ExpirationTime,
|
||||
suspenseConfig: null | SuspenseConfig,
|
||||
): void {
|
||||
if (
|
||||
expirationTime < workInProgressRootLatestProcessedExpirationTime &&
|
||||
expirationTime > Idle
|
||||
) {
|
||||
workInProgressRootLatestProcessedExpirationTime = expirationTime;
|
||||
}
|
||||
if (suspenseConfig !== null) {
|
||||
if (
|
||||
expirationTime < workInProgressRootLatestSuspenseTimeout &&
|
||||
expirationTime > Idle
|
||||
) {
|
||||
workInProgressRootLatestSuspenseTimeout = expirationTime;
|
||||
// Most of the time we only have one config and getting wrong is not bad.
|
||||
workInProgressRootCanSuspendUsingConfig = suspenseConfig;
|
||||
// Anything lower pri than Idle is not an update, so we should skip it.
|
||||
if (eventTime > Idle) {
|
||||
// Track the most recent event time of all updates processed in this batch.
|
||||
if (workInProgressRootLatestProcessedEventTime > eventTime) {
|
||||
workInProgressRootLatestProcessedEventTime = eventTime;
|
||||
}
|
||||
|
||||
// Track the largest/latest timeout deadline in this batch.
|
||||
// TODO: If there are two transitions in the same batch, shouldn't we
|
||||
// choose the smaller one? Maybe this is because when an intermediate
|
||||
// transition is superseded, we should ignore its suspense config, but
|
||||
// we don't currently.
|
||||
if (suspenseConfig !== null) {
|
||||
// If `timeoutMs` is not specified, we default to 5 seconds. We have to
|
||||
// resolve this default here because `suspenseConfig` is owned
|
||||
// by userspace.
|
||||
// TODO: Store this on the root instead (transition -> timeoutMs)
|
||||
// TODO: Should this default to a JND instead?
|
||||
const timeoutMs = suspenseConfig.timeoutMs | 0 || LOW_PRIORITY_EXPIRATION;
|
||||
const timeoutTime = computeSuspenseTimeout(eventTime, timeoutMs);
|
||||
if (timeoutTime < workInProgressRootLatestSuspenseTimeout) {
|
||||
workInProgressRootLatestSuspenseTimeout = timeoutTime;
|
||||
workInProgressRootCanSuspendUsingConfig = suspenseConfig;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1372,27 +1374,6 @@ export function renderHasNotSuspendedYet(): boolean {
|
||||
return workInProgressRootExitStatus === RootIncomplete;
|
||||
}
|
||||
|
||||
function inferTimeFromExpirationTime(expirationTime: ExpirationTime): number {
|
||||
// We don't know exactly when the update was scheduled, but we can infer an
|
||||
// approximate start time from the expiration time.
|
||||
const earliestExpirationTimeMs = expirationTimeToMs(expirationTime);
|
||||
return earliestExpirationTimeMs - LOW_PRIORITY_EXPIRATION;
|
||||
}
|
||||
|
||||
function inferTimeFromExpirationTimeWithSuspenseConfig(
|
||||
expirationTime: ExpirationTime,
|
||||
suspenseConfig: SuspenseConfig,
|
||||
): number {
|
||||
// We don't know exactly when the update was scheduled, but we can infer an
|
||||
// approximate start time from the expiration time by subtracting the timeout
|
||||
// that was added to the event time.
|
||||
const earliestExpirationTimeMs = expirationTimeToMs(expirationTime);
|
||||
return (
|
||||
earliestExpirationTimeMs -
|
||||
(suspenseConfig.timeoutMs | 0 || LOW_PRIORITY_EXPIRATION)
|
||||
);
|
||||
}
|
||||
|
||||
function renderRootSync(root, expirationTime) {
|
||||
const prevExecutionContext = executionContext;
|
||||
executionContext |= RenderContext;
|
||||
@@ -2540,7 +2521,7 @@ export function pingSuspendedRoot(
|
||||
if (
|
||||
workInProgressRootExitStatus === RootSuspendedWithDelay ||
|
||||
(workInProgressRootExitStatus === RootSuspended &&
|
||||
workInProgressRootLatestProcessedExpirationTime === Sync &&
|
||||
workInProgressRootLatestProcessedEventTime === Sync &&
|
||||
now() - globalMostRecentFallbackTime < FALLBACK_THROTTLE_MS)
|
||||
) {
|
||||
// Restart from the root. Don't need to schedule a ping because
|
||||
@@ -2679,10 +2660,7 @@ function computeMsUntilSuspenseLoadingDelay(
|
||||
|
||||
// Compute the time until this render pass would expire.
|
||||
const currentTimeMs: number = now();
|
||||
const eventTimeMs: number = inferTimeFromExpirationTimeWithSuspenseConfig(
|
||||
mostRecentEventTime,
|
||||
suspenseConfig,
|
||||
);
|
||||
const eventTimeMs: number = expirationTimeToMs(mostRecentEventTime);
|
||||
const timeElapsed = currentTimeMs - eventTimeMs;
|
||||
if (timeElapsed <= busyDelayMs) {
|
||||
// If we haven't yet waited longer than the initial delay, we don't
|
||||
|
||||
@@ -110,6 +110,9 @@ import {getCurrentPriorityLevel} from './SchedulerWithReactIntegration.new';
|
||||
import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
|
||||
|
||||
export type Update<State> = {|
|
||||
// TODO: Temporary field. Will remove this by storing a map of
|
||||
// transition -> event time on the root.
|
||||
eventTime: ExpirationTime,
|
||||
expirationTime: ExpirationTime,
|
||||
suspenseConfig: null | SuspenseConfig,
|
||||
|
||||
@@ -123,7 +126,9 @@ export type Update<State> = {|
|
||||
priority?: ReactPriorityLevel,
|
||||
|};
|
||||
|
||||
type SharedQueue<State> = {|pending: Update<State> | null|};
|
||||
type SharedQueue<State> = {|
|
||||
pending: Update<State> | null,
|
||||
|};
|
||||
|
||||
export type UpdateQueue<State> = {|
|
||||
baseState: State,
|
||||
@@ -187,10 +192,12 @@ export function cloneUpdateQueue<State>(
|
||||
}
|
||||
|
||||
export function createUpdate(
|
||||
eventTime: ExpirationTime,
|
||||
expirationTime: ExpirationTime,
|
||||
suspenseConfig: null | SuspenseConfig,
|
||||
): Update<*> {
|
||||
const update: Update<*> = {
|
||||
eventTime,
|
||||
expirationTime,
|
||||
suspenseConfig,
|
||||
|
||||
@@ -268,6 +275,7 @@ export function enqueueCapturedUpdate<State>(
|
||||
let update = firstBaseUpdate;
|
||||
do {
|
||||
const clone: Update<State> = {
|
||||
eventTime: update.eventTime,
|
||||
expirationTime: update.expirationTime,
|
||||
suspenseConfig: update.suspenseConfig,
|
||||
|
||||
@@ -471,13 +479,15 @@ export function processUpdateQueue<State>(
|
||||
|
||||
let update = firstBaseUpdate;
|
||||
do {
|
||||
const updateEventTime = update.eventTime;
|
||||
const updateExpirationTime = update.expirationTime;
|
||||
if (updateExpirationTime < renderExpirationTime) {
|
||||
// Priority is insufficient. Skip this update. If this is the first
|
||||
// skipped update, the previous update/state is the new base
|
||||
// update/state.
|
||||
const clone: Update<State> = {
|
||||
expirationTime: update.expirationTime,
|
||||
eventTime: updateEventTime,
|
||||
expirationTime: updateExpirationTime,
|
||||
suspenseConfig: update.suspenseConfig,
|
||||
|
||||
tag: update.tag,
|
||||
@@ -501,6 +511,7 @@ export function processUpdateQueue<State>(
|
||||
|
||||
if (newLastBaseUpdate !== null) {
|
||||
const clone: Update<State> = {
|
||||
eventTime: updateEventTime,
|
||||
expirationTime: Sync, // This update is going to be committed so we never want uncommit it.
|
||||
suspenseConfig: update.suspenseConfig,
|
||||
|
||||
@@ -519,10 +530,7 @@ export function processUpdateQueue<State>(
|
||||
// TODO: We should skip this update if it was already committed but currently
|
||||
// we have no way of detecting the difference between a committed and suspended
|
||||
// update here.
|
||||
markRenderEventTimeAndConfig(
|
||||
updateExpirationTime,
|
||||
update.suspenseConfig,
|
||||
);
|
||||
markRenderEventTimeAndConfig(updateEventTime, update.suspenseConfig);
|
||||
|
||||
// Process this update.
|
||||
newState = getStateFromUpdate(
|
||||
|
||||
+10
-4
@@ -2843,6 +2843,9 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
span('Before... Pending: true'),
|
||||
]);
|
||||
|
||||
// Resolve the promise. The whole tree has now completed. However,
|
||||
// because we exceeded the busy threshold, we won't commit the
|
||||
// result yet.
|
||||
Scheduler.unstable_advanceTime(1000);
|
||||
await advanceTimers(1000);
|
||||
expect(Scheduler).toHaveYielded([
|
||||
@@ -2853,13 +2856,16 @@ describe('ReactHooksWithNoopRenderer', () => {
|
||||
span('Before... Pending: true'),
|
||||
]);
|
||||
|
||||
Scheduler.unstable_advanceTime(1000);
|
||||
await advanceTimers(1000);
|
||||
// Advance time until just before the `busyMinDuration` threshold.
|
||||
Scheduler.unstable_advanceTime(999);
|
||||
await advanceTimers(999);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('Before... Pending: true'),
|
||||
]);
|
||||
Scheduler.unstable_advanceTime(250);
|
||||
await advanceTimers(250);
|
||||
|
||||
// Advance time just a bit more. Now we complete the transition.
|
||||
Scheduler.unstable_advanceTime(300);
|
||||
await advanceTimers(300);
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
span('After... Pending: false'),
|
||||
]);
|
||||
|
||||
@@ -498,6 +498,13 @@ describe('ReactSuspensePlaceholder', () => {
|
||||
</Suspense>
|
||||
</>,
|
||||
);
|
||||
|
||||
// TODO: This is here only to shift us into the next JND bucket. A
|
||||
// consequence of AsyncText relying on the same timer queue as React's
|
||||
// internal Suspense timer. We should decouple our AsyncText helpers
|
||||
// from timers.
|
||||
Scheduler.unstable_advanceTime(100);
|
||||
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'App',
|
||||
'Suspending',
|
||||
|
||||
+25
-37
@@ -2580,22 +2580,17 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
// but at this scope we should suspend for longer.
|
||||
Scheduler.unstable_next(() => ReactNoop.render(<App page="C" />));
|
||||
},
|
||||
{timeoutMs: 2000},
|
||||
{timeoutMs: 60000},
|
||||
);
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'Suspend! [C]',
|
||||
'Loading...',
|
||||
'Suspend! [C]',
|
||||
'Loading...',
|
||||
]);
|
||||
expect(Scheduler).toFlushAndYield(['B', 'Suspend! [C]', 'Loading...']);
|
||||
expect(ReactNoop.getChildren()).toEqual([span('B')]);
|
||||
Scheduler.unstable_advanceTime(1200);
|
||||
await advanceTimers(1200);
|
||||
// Even after a second, we have still not yet flushed the loading state.
|
||||
expect(ReactNoop.getChildren()).toEqual([span('B')]);
|
||||
Scheduler.unstable_advanceTime(1200);
|
||||
await advanceTimers(1200);
|
||||
// After the two second timeout we show the loading state.
|
||||
Scheduler.unstable_advanceTime(60000);
|
||||
await advanceTimers(60000);
|
||||
// After the timeout we show the loading state.
|
||||
expect(ReactNoop.getChildren()).toEqual([
|
||||
hiddenSpan('B'),
|
||||
span('Loading...'),
|
||||
@@ -2698,23 +2693,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
});
|
||||
|
||||
it('supports delaying a busy spinner from disappearing', async () => {
|
||||
function useLoadingIndicator(config) {
|
||||
const [isLoading, setLoading] = React.useState(false);
|
||||
const start = React.useCallback(
|
||||
cb => {
|
||||
setLoading(true);
|
||||
Scheduler.unstable_next(() =>
|
||||
React.unstable_withSuspenseConfig(() => {
|
||||
setLoading(false);
|
||||
cb();
|
||||
}, config),
|
||||
);
|
||||
},
|
||||
[setLoading, config],
|
||||
);
|
||||
return [isLoading, start];
|
||||
}
|
||||
|
||||
const SUSPENSE_CONFIG = {
|
||||
timeoutMs: 10000,
|
||||
busyDelayMs: 500,
|
||||
@@ -2725,7 +2703,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
|
||||
function App() {
|
||||
const [page, setPage] = React.useState('A');
|
||||
const [isLoading, startLoading] = useLoadingIndicator(SUSPENSE_CONFIG);
|
||||
const [startLoading, isLoading] = React.useTransition(SUSPENSE_CONFIG);
|
||||
transitionToPage = nextPage => startLoading(() => setPage(nextPage));
|
||||
return (
|
||||
<Fragment>
|
||||
@@ -2907,7 +2885,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
() => {
|
||||
ReactNoop.render(<App showContent={true} />);
|
||||
},
|
||||
{timeoutMs: 2000},
|
||||
{timeoutMs: 2500},
|
||||
);
|
||||
|
||||
expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
|
||||
@@ -3763,12 +3741,17 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
it.experimental(
|
||||
'does not get stuck in pending state with render phase updates',
|
||||
async () => {
|
||||
let setTextWithTransition;
|
||||
let setTextWithShortTransition;
|
||||
let setTextWithLongTransition;
|
||||
|
||||
function App() {
|
||||
const [startTransition, isPending] = React.useTransition({
|
||||
const [startShortTransition, isPending1] = React.useTransition({
|
||||
timeoutMs: 5000,
|
||||
});
|
||||
const [startLongTransition, isPending2] = React.useTransition({
|
||||
timeoutMs: 30000,
|
||||
});
|
||||
const isPending = isPending1 || isPending2;
|
||||
const [text, setText] = React.useState('');
|
||||
const [mirror, setMirror] = React.useState('');
|
||||
|
||||
@@ -3777,8 +3760,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
setMirror(text);
|
||||
}
|
||||
|
||||
setTextWithTransition = value => {
|
||||
startTransition(() => {
|
||||
setTextWithShortTransition = value => {
|
||||
startShortTransition(() => {
|
||||
setText(value);
|
||||
});
|
||||
};
|
||||
setTextWithLongTransition = value => {
|
||||
startLongTransition(() => {
|
||||
setText(value);
|
||||
});
|
||||
};
|
||||
@@ -3808,9 +3796,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
|
||||
// Update to "a". That will suspend.
|
||||
await ReactNoop.act(async () => {
|
||||
setTextWithTransition('a');
|
||||
// Let it expire. This is important for the repro.
|
||||
Scheduler.unstable_advanceTime(1000);
|
||||
setTextWithShortTransition('a');
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
'Pending...',
|
||||
'',
|
||||
@@ -3828,10 +3814,12 @@ describe('ReactSuspenseWithNoopRenderer', () => {
|
||||
|
||||
// Update to "b". That will suspend, too.
|
||||
await ReactNoop.act(async () => {
|
||||
setTextWithTransition('b');
|
||||
setTextWithLongTransition('b');
|
||||
expect(Scheduler).toFlushAndYield([
|
||||
// Neither is resolved yet.
|
||||
'Pending...',
|
||||
'',
|
||||
'Pending...',
|
||||
'Suspend! [a]',
|
||||
'Loading...',
|
||||
'Suspend! [b]',
|
||||
|
||||
Reference in New Issue
Block a user