@samitouri / QOS-React-2 / commits / b04c7fa28c

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.

Andrew Clark committed Apr 8, 2020 at 15:25 UTC b04c7fa28cadaa76fd8390963fe2b8d2a7962c50
11 files changed +136 -145
packages/react-reconciler/src/ReactFiberClassComponent.new.js
+3 -3
@@ -196,7 +196,7 @@ const classComponentUpdater = {
196 suspenseConfig,
197 );
198
199 - const update = createUpdate(expirationTime, suspenseConfig);
199 + const update = createUpdate(currentTime, expirationTime, suspenseConfig);
200 update.payload = payload;
201 if (callback !== undefined && callback !== null) {
202 if (__DEV__) {
@@ -218,7 +218,7 @@ const classComponentUpdater = {
218 suspenseConfig,
219 );
220
221 - const update = createUpdate(expirationTime, suspenseConfig);
221 + const update = createUpdate(currentTime, expirationTime, suspenseConfig);
222 update.tag = ReplaceState;
223 update.payload = payload;
224
@@ -242,7 +242,7 @@ const classComponentUpdater = {
242 suspenseConfig,
243 );
244
245 - const update = createUpdate(expirationTime, suspenseConfig);
245 + const update = createUpdate(currentTime, expirationTime, suspenseConfig);
246 update.tag = ForceUpdate;
247
248 if (callback !== undefined && callback !== null) {
packages/react-reconciler/src/ReactFiberExpirationTime.new.js
+6 -7
@@ -35,6 +35,8 @@ export const Idle = 2;
35 // Continuous Hydration is slightly higher than Idle and is used to increase
36 // priority of hover targets.
37 export const ContinuousHydration = 3;
38 +export const LongTransition = 49999;
39 +export const ShortTransition = 99999;
40 export const Sync = MAX_SIGNED_31_BIT_INT;
41 export const Batched = Sync - 1;
42
@@ -84,16 +86,13 @@ export function computeAsyncExpiration(
86 );
87 }
88
87 -export function computeSuspenseExpiration(
89 +export function computeSuspenseTimeout(
90 currentTime: ExpirationTime,
91 timeoutMs: number,
92 ): ExpirationTime {
91 - // TODO: Should we warn if timeoutMs is lower than the normal pri expiration time?
92 - return computeExpirationBucket(
93 - currentTime,
94 - timeoutMs,
95 - LOW_PRIORITY_BATCH_SIZE,
96 - );
93 + const currentTimeMs = expirationTimeToMs(currentTime);
94 + const deadlineMs = currentTimeMs + timeoutMs;
95 + return msToExpirationTime(deadlineMs);
96 }
97
98 // We intentionally set a higher expiration time for interactive updates in
packages/react-reconciler/src/ReactFiberHooks.new.js
+11 -6
@@ -96,6 +96,9 @@ import {getIsRendering} from './ReactCurrentFiber';
96 const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
97
98 type Update<S, A> = {|
99 + // TODO: Temporary field. Will remove this by storing a map of
100 + // transition -> start time on the root.
101 + eventTime: ExpirationTime,
102 expirationTime: ExpirationTime,
103 suspenseConfig: null | SuspenseConfig,
104 action: A,
@@ -715,14 +718,17 @@ function updateReducer<S, I, A>(
718 let newBaseQueueLast = null;
719 let update = first;
720 do {
721 + const suspenseConfig = update.suspenseConfig;
722 const updateExpirationTime = update.expirationTime;
723 + const updateEventTime = update.eventTime;
724 if (updateExpirationTime < renderExpirationTime) {
725 // Priority is insufficient. Skip this update. If this is the first
726 // skipped update, the previous update/state is the new base
727 // update/state.
728 const clone: Update<S, A> = {
724 - expirationTime: update.expirationTime,
725 - suspenseConfig: update.suspenseConfig,
729 + eventTime: updateEventTime,
730 + expirationTime: updateExpirationTime,
731 + suspenseConfig: suspenseConfig,
732 action: update.action,
733 eagerReducer: update.eagerReducer,
734 eagerState: update.eagerState,
@@ -744,6 +750,7 @@ function updateReducer<S, I, A>(
750
751 if (newBaseQueueLast !== null) {
752 const clone: Update<S, A> = {
753 + eventTime: updateEventTime,
754 expirationTime: Sync, // This update is going to be committed so we never want uncommit it.
755 suspenseConfig: update.suspenseConfig,
756 action: update.action,
@@ -760,10 +767,7 @@ function updateReducer<S, I, A>(
767 // TODO: We should skip this update if it was already committed but currently
768 // we have no way of detecting the difference between a committed and suspended
769 // update here.
763 - markRenderEventTimeAndConfig(
764 - updateExpirationTime,
765 - update.suspenseConfig,
766 - );
770 + markRenderEventTimeAndConfig(updateEventTime, suspenseConfig);
771
772 // Process this update.
773 if (update.eagerReducer === reducer) {
@@ -1651,6 +1655,7 @@ function dispatchAction<S, A>(
1655 );
1656
1657 const update: Update<S, A> = {
1658 + eventTime: currentTime,
1659 expirationTime,
1660 suspenseConfig,
1661 action,
packages/react-reconciler/src/ReactFiberNewContext.new.js
+1 -1
@@ -206,7 +206,7 @@ export function propagateContextChange(
206
207 if (fiber.tag === ClassComponent) {
208 // Schedule a force update on the work-in-progress.
209 - const update = createUpdate(renderExpirationTime, null);
209 + const update = createUpdate(NoWork, renderExpirationTime, null);
210 update.tag = ForceUpdate;
211 // TODO: Because we don't have a work-in-progress, this will add the
212 // update to the current fiber, too, which means it will persist even if
packages/react-reconciler/src/ReactFiberReconciler.new.js
+1 -1
@@ -271,7 +271,7 @@ export function updateContainer(
271 }
272 }
273
274 - const update = createUpdate(expirationTime, suspenseConfig);
274 + const update = createUpdate(currentTime, expirationTime, suspenseConfig);
275 // Caution: React DevTools currently depends on this property
276 // being called "element".
277 update.payload = {element};
packages/react-reconciler/src/ReactFiberThrow.new.js
+4 -4
@@ -56,7 +56,7 @@ import {
56 } from './ReactFiberWorkLoop.new';
57 import {logCapturedError} from './ReactFiberErrorLogger';
58
59 -import {Sync} from './ReactFiberExpirationTime.new';
59 +import {Sync, NoWork} from './ReactFiberExpirationTime.new';
60
61 const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
62
@@ -65,7 +65,7 @@ function createRootErrorUpdate(
65 errorInfo: CapturedValue<mixed>,
66 expirationTime: ExpirationTime,
67 ): Update<mixed> {
68 - const update = createUpdate(expirationTime, null);
68 + const update = createUpdate(NoWork, expirationTime, null);
69 // Unmount the root by rendering null.
70 update.tag = CaptureUpdate;
71 // Caution: React DevTools currently depends on this property
@@ -84,7 +84,7 @@ function createClassErrorUpdate(
84 errorInfo: CapturedValue<mixed>,
85 expirationTime: ExpirationTime,
86 ): Update<mixed> {
87 - const update = createUpdate(expirationTime, null);
87 + const update = createUpdate(NoWork, expirationTime, null);
88 update.tag = CaptureUpdate;
89 const getDerivedStateFromError = fiber.type.getDerivedStateFromError;
90 if (typeof getDerivedStateFromError === 'function') {
@@ -260,7 +260,7 @@ function throwException(
260 // When we try rendering again, we should not reuse the current fiber,
261 // since it's known to be in an inconsistent state. Use a force update to
262 // prevent a bail out.
263 - const update = createUpdate(Sync, null);
263 + const update = createUpdate(NoWork, Sync, null);
264 update.tag = ForceUpdate;
265 enqueueUpdate(sourceFiber, update);
266 }
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+54 -76
@@ -119,11 +119,13 @@ import {
119 expirationTimeToMs,
120 computeInteractiveExpiration,
121 computeAsyncExpiration,
122 - computeSuspenseExpiration,
123 - inferPriorityFromExpirationTime,
122 + computeSuspenseTimeout,
123 LOW_PRIORITY_EXPIRATION,
124 + inferPriorityFromExpirationTime,
125 Batched,
126 Idle,
127 + ShortTransition,
128 + LongTransition,
129 } from './ReactFiberExpirationTime.new';
130 import {beginWork as originalBeginWork} from './ReactFiberBeginWork.new';
131 import {completeWork} from './ReactFiberCompleteWork.new';
@@ -220,7 +222,7 @@ let workInProgressRootFatalError: mixed = null;
222 // This is conceptually a time stamp but expressed in terms of an ExpirationTime
223 // because we deal mostly with expiration times in the hot path, so this avoids
224 // the conversion happening in the hot path.
223 -let workInProgressRootLatestProcessedExpirationTime: ExpirationTime = Sync;
225 +let workInProgressRootLatestProcessedEventTime: ExpirationTime = Sync;
226 let workInProgressRootLatestSuspenseTimeout: ExpirationTime = Sync;
227 let workInProgressRootCanSuspendUsingConfig: null | SuspenseConfig = null;
228 // The work left over by components that were visited during this render. Only
@@ -328,11 +330,16 @@ export function computeExpirationForFiber(
330
331 let expirationTime;
332 if (suspenseConfig !== null) {
331 - // Compute an expiration time based on the Suspense timeout.
332 - expirationTime = computeSuspenseExpiration(
333 - currentTime,
334 - suspenseConfig.timeoutMs | 0 || LOW_PRIORITY_EXPIRATION,
335 - );
333 + // If there's a SuspenseConfig, choose an expiration time that's lower
334 + // priority than a normal concurrent update (regardless of the current
335 + // Scheduler priority.) Timeouts larger than 10 seconds move one level lower
336 + // than that.
337 + const timeoutMs = suspenseConfig.timeoutMs;
338 + expirationTime =
339 + // TODO: This will coerce numbers larger than 31 bits to 0.
340 + timeoutMs === undefined || (timeoutMs | 0) < 10000
341 + ? ShortTransition
342 + : LongTransition;
343 } else {
344 // Compute an expiration time based on the Scheduler priority.
345 switch (priorityLevel) {
@@ -379,7 +386,7 @@ export function scheduleUpdateOnFiber(
386 const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);
387 if (root === null) {
388 warnAboutUpdateOnUnmountedFiberInDEV(fiber);
382 - return;
389 + return null;
390 }
391
392 // TODO: computeExpirationForFiber also reads the priority. Pass the
@@ -728,7 +735,7 @@ function finishConcurrentRender(
735 // have a new loading state ready. We want to ensure that we commit
736 // that as soon as possible.
737 const hasNotProcessedNewUpdates =
731 - workInProgressRootLatestProcessedExpirationTime === Sync;
738 + workInProgressRootLatestProcessedEventTime === Sync;
739 if (
740 hasNotProcessedNewUpdates &&
741 // do not delay if we're inside an act() scope
@@ -836,34 +843,19 @@ function finishConcurrentRender(
843 // can use as the timeout.
844 msUntilTimeout =
845 expirationTimeToMs(workInProgressRootLatestSuspenseTimeout) - now();
839 - } else if (workInProgressRootLatestProcessedExpirationTime === Sync) {
846 + } else if (workInProgressRootLatestProcessedEventTime === Sync) {
847 // This should never normally happen because only new updates
848 // cause delayed states, so we should have processed something.
849 // However, this could also happen in an offscreen tree.
850 msUntilTimeout = 0;
851 } else {
845 - // If we don't have a suspense config, we're going to use a
846 - // heuristic to determine how long we can suspend.
847 - const eventTimeMs: number = inferTimeFromExpirationTime(
848 - workInProgressRootLatestProcessedExpirationTime,
852 + // If we didn't process a suspense config, compute a JND based on
853 + // the amount of time elapsed since the most recent event time.
854 + const eventTimeMs = expirationTimeToMs(
855 + workInProgressRootLatestProcessedEventTime,
856 );
850 - const currentTimeMs = now();
851 - const timeUntilExpirationMs =
852 - expirationTimeToMs(expirationTime) - currentTimeMs;
853 - let timeElapsed = currentTimeMs - eventTimeMs;
854 - if (timeElapsed < 0) {
855 - // We get this wrong some time since we estimate the time.
856 - timeElapsed = 0;
857 - }
858 -
859 - msUntilTimeout = jnd(timeElapsed) - timeElapsed;
860 -
861 - // Clamp the timeout to the expiration time. TODO: Once the
862 - // event time is exact instead of inferred from expiration time
863 - // we don't need this.
864 - if (timeUntilExpirationMs < msUntilTimeout) {
865 - msUntilTimeout = timeUntilExpirationMs;
866 - }
857 + const timeElapsedMs = now() - eventTimeMs;
858 + msUntilTimeout = jnd(timeElapsedMs) - timeElapsedMs;
859 }
860
861 // Don't bother with a very short suspense time.
@@ -891,7 +883,7 @@ function finishConcurrentRender(
883 flushSuspenseFallbacksInTests &&
884 IsThisRendererActing.current
885 ) &&
894 - workInProgressRootLatestProcessedExpirationTime !== Sync &&
886 + workInProgressRootLatestProcessedEventTime !== Sync &&
887 workInProgressRootCanSuspendUsingConfig !== null
888 ) {
889 // If we have exceeded the minimum loading delay, which probably
@@ -899,7 +891,7 @@ function finishConcurrentRender(
891 // a bit longer to ensure that the spinner is shown for
892 // enough time.
893 const msUntilTimeout = computeMsUntilSuspenseLoadingDelay(
902 - workInProgressRootLatestProcessedExpirationTime,
894 + workInProgressRootLatestProcessedEventTime,
895 expirationTime,
896 workInProgressRootCanSuspendUsingConfig,
897 );
@@ -1193,7 +1185,7 @@ function prepareFreshStack(root, expirationTime) {
1185 renderExpirationTime = expirationTime;
1186 workInProgressRootExitStatus = RootIncomplete;
1187 workInProgressRootFatalError = null;
1196 - workInProgressRootLatestProcessedExpirationTime = Sync;
1188 + workInProgressRootLatestProcessedEventTime = Sync;
1189 workInProgressRootLatestSuspenseTimeout = Sync;
1190 workInProgressRootCanSuspendUsingConfig = null;
1191 workInProgressRootNextUnprocessedUpdateTime = NoWork;
@@ -1298,23 +1290,33 @@ export function markCommitTimeOfFallback() {
1290 }
1291
1292 export function markRenderEventTimeAndConfig(
1301 - expirationTime: ExpirationTime,
1293 + eventTime: ExpirationTime,
1294 suspenseConfig: null | SuspenseConfig,
1295 ): void {
1304 - if (
1305 - expirationTime < workInProgressRootLatestProcessedExpirationTime &&
1306 - expirationTime > Idle
1307 - ) {
1308 - workInProgressRootLatestProcessedExpirationTime = expirationTime;
1309 - }
1310 - if (suspenseConfig !== null) {
1311 - if (
1312 - expirationTime < workInProgressRootLatestSuspenseTimeout &&
1313 - expirationTime > Idle
1314 - ) {
1315 - workInProgressRootLatestSuspenseTimeout = expirationTime;
1316 - // Most of the time we only have one config and getting wrong is not bad.
1317 - workInProgressRootCanSuspendUsingConfig = suspenseConfig;
1296 + // Anything lower pri than Idle is not an update, so we should skip it.
1297 + if (eventTime > Idle) {
1298 + // Track the most recent event time of all updates processed in this batch.
1299 + if (workInProgressRootLatestProcessedEventTime > eventTime) {
1300 + workInProgressRootLatestProcessedEventTime = eventTime;
1301 + }
1302 +
1303 + // Track the largest/latest timeout deadline in this batch.
1304 + // TODO: If there are two transitions in the same batch, shouldn't we
1305 + // choose the smaller one? Maybe this is because when an intermediate
1306 + // transition is superseded, we should ignore its suspense config, but
1307 + // we don't currently.
1308 + if (suspenseConfig !== null) {
1309 + // If `timeoutMs` is not specified, we default to 5 seconds. We have to
1310 + // resolve this default here because `suspenseConfig` is owned
1311 + // by userspace.
1312 + // TODO: Store this on the root instead (transition -> timeoutMs)
1313 + // TODO: Should this default to a JND instead?
1314 + const timeoutMs = suspenseConfig.timeoutMs | 0 || LOW_PRIORITY_EXPIRATION;
1315 + const timeoutTime = computeSuspenseTimeout(eventTime, timeoutMs);
1316 + if (timeoutTime < workInProgressRootLatestSuspenseTimeout) {
1317 + workInProgressRootLatestSuspenseTimeout = timeoutTime;
1318 + workInProgressRootCanSuspendUsingConfig = suspenseConfig;
1319 + }
1320 }
1321 }
1322 }
@@ -1372,27 +1374,6 @@ export function renderHasNotSuspendedYet(): boolean {
1374 return workInProgressRootExitStatus === RootIncomplete;
1375 }
1376
1375 -function inferTimeFromExpirationTime(expirationTime: ExpirationTime): number {
1376 - // We don't know exactly when the update was scheduled, but we can infer an
1377 - // approximate start time from the expiration time.
1378 - const earliestExpirationTimeMs = expirationTimeToMs(expirationTime);
1379 - return earliestExpirationTimeMs - LOW_PRIORITY_EXPIRATION;
1380 -}
1381 -
1382 -function inferTimeFromExpirationTimeWithSuspenseConfig(
1383 - expirationTime: ExpirationTime,
1384 - suspenseConfig: SuspenseConfig,
1385 -): number {
1386 - // We don't know exactly when the update was scheduled, but we can infer an
1387 - // approximate start time from the expiration time by subtracting the timeout
1388 - // that was added to the event time.
1389 - const earliestExpirationTimeMs = expirationTimeToMs(expirationTime);
1390 - return (
1391 - earliestExpirationTimeMs -
1392 - (suspenseConfig.timeoutMs | 0 || LOW_PRIORITY_EXPIRATION)
1393 - );
1394 -}
1395 -
1377 function renderRootSync(root, expirationTime) {
1378 const prevExecutionContext = executionContext;
1379 executionContext |= RenderContext;
@@ -2540,7 +2521,7 @@ export function pingSuspendedRoot(
2521 if (
2522 workInProgressRootExitStatus === RootSuspendedWithDelay ||
2523 (workInProgressRootExitStatus === RootSuspended &&
2543 - workInProgressRootLatestProcessedExpirationTime === Sync &&
2524 + workInProgressRootLatestProcessedEventTime === Sync &&
2525 now() - globalMostRecentFallbackTime < FALLBACK_THROTTLE_MS)
2526 ) {
2527 // Restart from the root. Don't need to schedule a ping because
@@ -2679,10 +2660,7 @@ function computeMsUntilSuspenseLoadingDelay(
2660
2661 // Compute the time until this render pass would expire.
2662 const currentTimeMs: number = now();
2682 - const eventTimeMs: number = inferTimeFromExpirationTimeWithSuspenseConfig(
2683 - mostRecentEventTime,
2684 - suspenseConfig,
2685 - );
2663 + const eventTimeMs: number = expirationTimeToMs(mostRecentEventTime);
2664 const timeElapsed = currentTimeMs - eventTimeMs;
2665 if (timeElapsed <= busyDelayMs) {
2666 // If we haven't yet waited longer than the initial delay, we don't
packages/react-reconciler/src/ReactUpdateQueue.new.js
+14 -6
@@ -110,6 +110,9 @@ import {getCurrentPriorityLevel} from './SchedulerWithReactIntegration.new';
110 import {disableLogs, reenableLogs} from 'shared/ConsolePatchingDev';
111
112 export type Update<State> = {|
113 + // TODO: Temporary field. Will remove this by storing a map of
114 + // transition -> event time on the root.
115 + eventTime: ExpirationTime,
116 expirationTime: ExpirationTime,
117 suspenseConfig: null | SuspenseConfig,
118
@@ -123,7 +126,9 @@ export type Update<State> = {|
126 priority?: ReactPriorityLevel,
127 |};
128
126 -type SharedQueue<State> = {|pending: Update<State> | null|};
129 +type SharedQueue<State> = {|
130 + pending: Update<State> | null,
131 +|};
132
133 export type UpdateQueue<State> = {|
134 baseState: State,
@@ -187,10 +192,12 @@ export function cloneUpdateQueue<State>(
192 }
193
194 export function createUpdate(
195 + eventTime: ExpirationTime,
196 expirationTime: ExpirationTime,
197 suspenseConfig: null | SuspenseConfig,
198 ): Update<*> {
199 const update: Update<*> = {
200 + eventTime,
201 expirationTime,
202 suspenseConfig,
203
@@ -268,6 +275,7 @@ export function enqueueCapturedUpdate<State>(
275 let update = firstBaseUpdate;
276 do {
277 const clone: Update<State> = {
278 + eventTime: update.eventTime,
279 expirationTime: update.expirationTime,
280 suspenseConfig: update.suspenseConfig,
281
@@ -471,13 +479,15 @@ export function processUpdateQueue<State>(
479
480 let update = firstBaseUpdate;
481 do {
482 + const updateEventTime = update.eventTime;
483 const updateExpirationTime = update.expirationTime;
484 if (updateExpirationTime < renderExpirationTime) {
485 // Priority is insufficient. Skip this update. If this is the first
486 // skipped update, the previous update/state is the new base
487 // update/state.
488 const clone: Update<State> = {
480 - expirationTime: update.expirationTime,
489 + eventTime: updateEventTime,
490 + expirationTime: updateExpirationTime,
491 suspenseConfig: update.suspenseConfig,
492
493 tag: update.tag,
@@ -501,6 +511,7 @@ export function processUpdateQueue<State>(
511
512 if (newLastBaseUpdate !== null) {
513 const clone: Update<State> = {
514 + eventTime: updateEventTime,
515 expirationTime: Sync, // This update is going to be committed so we never want uncommit it.
516 suspenseConfig: update.suspenseConfig,
517
@@ -519,10 +530,7 @@ export function processUpdateQueue<State>(
530 // TODO: We should skip this update if it was already committed but currently
531 // we have no way of detecting the difference between a committed and suspended
532 // update here.
522 - markRenderEventTimeAndConfig(
523 - updateExpirationTime,
524 - update.suspenseConfig,
525 - );
533 + markRenderEventTimeAndConfig(updateEventTime, update.suspenseConfig);
534
535 // Process this update.
536 newState = getStateFromUpdate(
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js
+10 -4
@@ -2843,6 +2843,9 @@ describe('ReactHooksWithNoopRenderer', () => {
2843 span('Before... Pending: true'),
2844 ]);
2845
2846 + // Resolve the promise. The whole tree has now completed. However,
2847 + // because we exceeded the busy threshold, we won't commit the
2848 + // result yet.
2849 Scheduler.unstable_advanceTime(1000);
2850 await advanceTimers(1000);
2851 expect(Scheduler).toHaveYielded([
@@ -2853,13 +2856,16 @@ describe('ReactHooksWithNoopRenderer', () => {
2856 span('Before... Pending: true'),
2857 ]);
2858
2856 - Scheduler.unstable_advanceTime(1000);
2857 - await advanceTimers(1000);
2859 + // Advance time until just before the `busyMinDuration` threshold.
2860 + Scheduler.unstable_advanceTime(999);
2861 + await advanceTimers(999);
2862 expect(ReactNoop.getChildren()).toEqual([
2863 span('Before... Pending: true'),
2864 ]);
2861 - Scheduler.unstable_advanceTime(250);
2862 - await advanceTimers(250);
2865 +
2866 + // Advance time just a bit more. Now we complete the transition.
2867 + Scheduler.unstable_advanceTime(300);
2868 + await advanceTimers(300);
2869 expect(ReactNoop.getChildren()).toEqual([
2870 span('After... Pending: false'),
2871 ]);
packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js
+7
@@ -498,6 +498,13 @@ describe('ReactSuspensePlaceholder', () => {
498 </Suspense>
499 </>,
500 );
501 +
502 + // TODO: This is here only to shift us into the next JND bucket. A
503 + // consequence of AsyncText relying on the same timer queue as React's
504 + // internal Suspense timer. We should decouple our AsyncText helpers
505 + // from timers.
506 + Scheduler.unstable_advanceTime(100);
507 +
508 expect(Scheduler).toFlushAndYield([
509 'App',
510 'Suspending',
packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.internal.js
+25 -37
@@ -2580,22 +2580,17 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2580 // but at this scope we should suspend for longer.
2581 Scheduler.unstable_next(() => ReactNoop.render(<App page="C" />));
2582 },
2583 - {timeoutMs: 2000},
2583 + {timeoutMs: 60000},
2584 );
2585 - expect(Scheduler).toFlushAndYield([
2586 - 'Suspend! [C]',
2587 - 'Loading...',
2588 - 'Suspend! [C]',
2589 - 'Loading...',
2590 - ]);
2585 + expect(Scheduler).toFlushAndYield(['B', 'Suspend! [C]', 'Loading...']);
2586 expect(ReactNoop.getChildren()).toEqual([span('B')]);
2587 Scheduler.unstable_advanceTime(1200);
2588 await advanceTimers(1200);
2589 // Even after a second, we have still not yet flushed the loading state.
2590 expect(ReactNoop.getChildren()).toEqual([span('B')]);
2596 - Scheduler.unstable_advanceTime(1200);
2597 - await advanceTimers(1200);
2598 - // After the two second timeout we show the loading state.
2591 + Scheduler.unstable_advanceTime(60000);
2592 + await advanceTimers(60000);
2593 + // After the timeout we show the loading state.
2594 expect(ReactNoop.getChildren()).toEqual([
2595 hiddenSpan('B'),
2596 span('Loading...'),
@@ -2698,23 +2693,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2693 });
2694
2695 it('supports delaying a busy spinner from disappearing', async () => {
2701 - function useLoadingIndicator(config) {
2702 - const [isLoading, setLoading] = React.useState(false);
2703 - const start = React.useCallback(
2704 - cb => {
2705 - setLoading(true);
2706 - Scheduler.unstable_next(() =>
2707 - React.unstable_withSuspenseConfig(() => {
2708 - setLoading(false);
2709 - cb();
2710 - }, config),
2711 - );
2712 - },
2713 - [setLoading, config],
2714 - );
2715 - return [isLoading, start];
2716 - }
2717 -
2696 const SUSPENSE_CONFIG = {
2697 timeoutMs: 10000,
2698 busyDelayMs: 500,
@@ -2725,7 +2703,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2703
2704 function App() {
2705 const [page, setPage] = React.useState('A');
2728 - const [isLoading, startLoading] = useLoadingIndicator(SUSPENSE_CONFIG);
2706 + const [startLoading, isLoading] = React.useTransition(SUSPENSE_CONFIG);
2707 transitionToPage = nextPage => startLoading(() => setPage(nextPage));
2708 return (
2709 <Fragment>
@@ -2907,7 +2885,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
2885 () => {
2886 ReactNoop.render(<App showContent={true} />);
2887 },
2910 - {timeoutMs: 2000},
2888 + {timeoutMs: 2500},
2889 );
2890
2891 expect(Scheduler).toFlushAndYield(['Suspend! [A]', 'Loading...']);
@@ -3763,12 +3741,17 @@ describe('ReactSuspenseWithNoopRenderer', () => {
3741 it.experimental(
3742 'does not get stuck in pending state with render phase updates',
3743 async () => {
3766 - let setTextWithTransition;
3744 + let setTextWithShortTransition;
3745 + let setTextWithLongTransition;
3746
3747 function App() {
3769 - const [startTransition, isPending] = React.useTransition({
3748 + const [startShortTransition, isPending1] = React.useTransition({
3749 + timeoutMs: 5000,
3750 + });
3751 + const [startLongTransition, isPending2] = React.useTransition({
3752 timeoutMs: 30000,
3753 });
3754 + const isPending = isPending1 || isPending2;
3755 const [text, setText] = React.useState('');
3756 const [mirror, setMirror] = React.useState('');
3757
@@ -3777,8 +3760,13 @@ describe('ReactSuspenseWithNoopRenderer', () => {
3760 setMirror(text);
3761 }
3762
3780 - setTextWithTransition = value => {
3781 - startTransition(() => {
3763 + setTextWithShortTransition = value => {
3764 + startShortTransition(() => {
3765 + setText(value);
3766 + });
3767 + };
3768 + setTextWithLongTransition = value => {
3769 + startLongTransition(() => {
3770 setText(value);
3771 });
3772 };
@@ -3808,9 +3796,7 @@ describe('ReactSuspenseWithNoopRenderer', () => {
3796
3797 // Update to "a". That will suspend.
3798 await ReactNoop.act(async () => {
3811 - setTextWithTransition('a');
3812 - // Let it expire. This is important for the repro.
3813 - Scheduler.unstable_advanceTime(1000);
3799 + setTextWithShortTransition('a');
3800 expect(Scheduler).toFlushAndYield([
3801 'Pending...',
3802 '',
@@ -3828,10 +3814,12 @@ describe('ReactSuspenseWithNoopRenderer', () => {
3814
3815 // Update to "b". That will suspend, too.
3816 await ReactNoop.act(async () => {
3831 - setTextWithTransition('b');
3817 + setTextWithLongTransition('b');
3818 expect(Scheduler).toFlushAndYield([
3819 // Neither is resolved yet.
3820 'Pending...',
3821 + '',
3822 + 'Pending...',
3823 'Suspend! [a]',
3824 'Loading...',
3825 'Suspend! [b]',