Skip to content

Commit 8da0da0

Browse files
authoredAug 26, 2020
Disable timeoutMs argument (#19703)
* Remove distinction between long, short transitions We're removing the `timeoutMs` option, so there's no longer any distinction between "short" and "long" transitions. They're all treated the same. This commit doesn't remove `timeoutMs` yet, only combines the internal priority levels. * Disable `timeoutMs` argument tl;dr ----- - We're removing the `timeoutMs` argument from `useTransition`. - Transitions will either immediately switch to a skeleton/placeholder view (when loading new content) or wait indefinitely until the data resolves (when refreshing stale content). - This commit disables the `timeoutMS` so that the API has the desired semantics. It doesn't yet update the types or migrate all the test callers. I'll do those steps in follow-up PRs. Motivation ---------- Currently, transitions initiated by `startTransition` / `useTransition` accept a `timeoutMs` option. You can use this to control the maximum amount of time that a transition is allowed to delay before we give up and show a placeholder. What we've discovered is that, in practice, every transition falls into one of two categories: a **load** or a **refresh**: - **Loading a new screen**: show the next screen as soon as possible, even if the data hasn't finished loading. Use a skeleton/placeholder UI to show progress. - **Refreshing a screen that's already visible**: keep showing the current screen indefinitely, for as long as it takes to load the fresh data, even if the current data is stale. Use a pending state (and maybe a busy indicator) to show progress. In other words, transitions should either *delay indefinitely* (for a refresh) or they should show a placeholder *instantly* (for a load). There's not much use for transitions that are delayed for a small-but-noticeable amount of time. So, the plan is to remove the `timeoutMs` option. Instead, we'll assign an effective timeout of `0` for loads, and `Infinity` for refreshes. The mechanism for distinguishing a load from a refresh already exists in the current model. If a component suspends, and the nearest Suspense boundary hasn't already mounted, we treat that as a load, because there's nothing on the screen. However, if the nearest boundary is mounted, we treat that as a refresh, since it's already showing content. If you need to fix a transition to be treated as a load instead of a refresh, or vice versa, the solution will involve rearranging the location of your Suspense boundaries. It may also involve adding a key. We're still working on proper documentation for these patterns. In the meantime, please reach out to us if you run into problems that you're unsure how to fix. We will remove `timeoutMs` from `useDeferredValue`, too, and apply the same load versus refresh semantics to the update that spawns the deferred value. Note that there are other types of delays that are not related to transitions; for example, we will still throttle the appearance of nested placeholders (we refer to this as the placeholder "train model"), and we may still apply a Just Noticeable Difference heuristic (JND) in some cases. These aren't going anywhere. (Well, the JND heuristic might but for different reasons than those discussed above.)
1 parent 60ba723 commit 8da0da0

File tree

8 files changed

+254
-372
lines changed

8 files changed

+254
-372
lines changed
 

‎packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js

+13-4
Original file line numberDiff line numberDiff line change
@@ -779,10 +779,19 @@ function runActTests(label, render, unmount, rerender) {
779779
},
780780
{timeout: 5000},
781781
);
782-
// the spinner shows up regardless
783-
expect(
784-
document.querySelector('[data-test-id=spinner]'),
785-
).not.toBeNull();
782+
783+
if (label === 'concurrent mode') {
784+
// In Concurrent Mode, refresh transitions delay indefinitely.
785+
expect(document.querySelector('[data-test-id=spinner]')).toBeNull();
786+
} else {
787+
// In Legacy Mode and Blocking Mode, all fallbacks are forced to
788+
// display, even during a refresh transition.
789+
// TODO: Consider delaying indefinitely in Blocking Mode, to match
790+
// Concurrent Mode semantics.
791+
expect(
792+
document.querySelector('[data-test-id=spinner]'),
793+
).not.toBeNull();
794+
}
786795

787796
// resolve the promise
788797
await act(async () => {

‎packages/react-reconciler/src/ReactFiberLane.js

+40-91
Original file line numberDiff line numberDiff line change
@@ -43,23 +43,20 @@ import {
4343
NoPriority as NoSchedulerPriority,
4444
} from './SchedulerWithReactIntegration.new';
4545

46-
export const SyncLanePriority: LanePriority = 17;
47-
export const SyncBatchedLanePriority: LanePriority = 16;
46+
export const SyncLanePriority: LanePriority = 15;
47+
export const SyncBatchedLanePriority: LanePriority = 14;
4848

49-
const InputDiscreteHydrationLanePriority: LanePriority = 15;
50-
export const InputDiscreteLanePriority: LanePriority = 14;
49+
const InputDiscreteHydrationLanePriority: LanePriority = 13;
50+
export const InputDiscreteLanePriority: LanePriority = 12;
5151

52-
const InputContinuousHydrationLanePriority: LanePriority = 13;
53-
export const InputContinuousLanePriority: LanePriority = 12;
52+
const InputContinuousHydrationLanePriority: LanePriority = 11;
53+
export const InputContinuousLanePriority: LanePriority = 10;
5454

55-
const DefaultHydrationLanePriority: LanePriority = 11;
56-
export const DefaultLanePriority: LanePriority = 10;
55+
const DefaultHydrationLanePriority: LanePriority = 9;
56+
export const DefaultLanePriority: LanePriority = 8;
5757

58-
const TransitionShortHydrationLanePriority: LanePriority = 9;
59-
export const TransitionShortLanePriority: LanePriority = 8;
60-
61-
const TransitionLongHydrationLanePriority: LanePriority = 7;
62-
export const TransitionLongLanePriority: LanePriority = 6;
58+
const TransitionHydrationPriority: LanePriority = 7;
59+
export const TransitionPriority: LanePriority = 6;
6360

6461
const RetryLanePriority: LanePriority = 5;
6562

@@ -89,11 +86,8 @@ const InputContinuousLanes: Lanes = /* */ 0b0000000000000000000
8986
export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000100000000;
9087
export const DefaultLanes: Lanes = /* */ 0b0000000000000000000111000000000;
9188

92-
const TransitionShortHydrationLane: Lane = /* */ 0b0000000000000000001000000000000;
93-
const TransitionShortLanes: Lanes = /* */ 0b0000000000000011110000000000000;
94-
95-
const TransitionLongHydrationLane: Lane = /* */ 0b0000000000000100000000000000000;
96-
const TransitionLongLanes: Lanes = /* */ 0b0000000001111000000000000000000;
89+
const TransitionHydrationLane: Lane = /* */ 0b0000000000000000001000000000000;
90+
const TransitionLanes: Lanes = /* */ 0b0000000001111111110000000000000;
9791

9892
const RetryLanes: Lanes = /* */ 0b0000011110000000000000000000000;
9993

@@ -160,23 +154,14 @@ function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes {
160154
return_highestLanePriority = DefaultLanePriority;
161155
return defaultLanes;
162156
}
163-
if ((lanes & TransitionShortHydrationLane) !== NoLanes) {
164-
return_highestLanePriority = TransitionShortHydrationLanePriority;
165-
return TransitionShortHydrationLane;
166-
}
167-
const transitionShortLanes = TransitionShortLanes & lanes;
168-
if (transitionShortLanes !== NoLanes) {
169-
return_highestLanePriority = TransitionShortLanePriority;
170-
return transitionShortLanes;
157+
if ((lanes & TransitionHydrationLane) !== NoLanes) {
158+
return_highestLanePriority = TransitionHydrationPriority;
159+
return TransitionHydrationLane;
171160
}
172-
if ((lanes & TransitionLongHydrationLane) !== NoLanes) {
173-
return_highestLanePriority = TransitionLongHydrationLanePriority;
174-
return TransitionLongHydrationLane;
175-
}
176-
const transitionLongLanes = TransitionLongLanes & lanes;
177-
if (transitionLongLanes !== NoLanes) {
178-
return_highestLanePriority = TransitionLongLanePriority;
179-
return transitionLongLanes;
161+
const transitionLanes = TransitionLanes & lanes;
162+
if (transitionLanes !== NoLanes) {
163+
return_highestLanePriority = TransitionPriority;
164+
return transitionLanes;
180165
}
181166
const retryLanes = RetryLanes & lanes;
182167
if (retryLanes !== NoLanes) {
@@ -241,10 +226,8 @@ export function lanePriorityToSchedulerPriority(
241226
return UserBlockingSchedulerPriority;
242227
case DefaultHydrationLanePriority:
243228
case DefaultLanePriority:
244-
case TransitionShortHydrationLanePriority:
245-
case TransitionShortLanePriority:
246-
case TransitionLongHydrationLanePriority:
247-
case TransitionLongLanePriority:
229+
case TransitionHydrationPriority:
230+
case TransitionPriority:
248231
case SelectiveHydrationLanePriority:
249232
case RetryLanePriority:
250233
return NormalSchedulerPriority;
@@ -402,7 +385,7 @@ function computeExpirationTime(lane: Lane, currentTime: number) {
402385
if (priority >= InputContinuousLanePriority) {
403386
// User interactions should expire slightly more quickly.
404387
return currentTime + 1000;
405-
} else if (priority >= TransitionLongLanePriority) {
388+
} else if (priority >= TransitionPriority) {
406389
return currentTime + 5000;
407390
} else {
408391
// Anything idle priority or lower should never expire.
@@ -513,9 +496,7 @@ export function findUpdateLane(
513496
if (lane === NoLane) {
514497
// If all the default lanes are already being worked on, look for a
515498
// lane in the transition range.
516-
lane = pickArbitraryLane(
517-
(TransitionShortLanes | TransitionLongLanes) & ~wipLanes,
518-
);
499+
lane = pickArbitraryLane(TransitionLanes & ~wipLanes);
519500
if (lane === NoLane) {
520501
// All the transition lanes are taken, too. This should be very
521502
// rare, but as a last resort, pick a default lane. This will have
@@ -525,8 +506,7 @@ export function findUpdateLane(
525506
}
526507
return lane;
527508
}
528-
case TransitionShortLanePriority: // Should be handled by findTransitionLane instead
529-
case TransitionLongLanePriority:
509+
case TransitionPriority: // Should be handled by findTransitionLane instead
530510
case RetryLanePriority: // Should be handled by findRetryLane instead
531511
break;
532512
case IdleLanePriority:
@@ -548,48 +528,21 @@ export function findUpdateLane(
548528

549529
// To ensure consistency across multiple updates in the same event, this should
550530
// be pure function, so that it always returns the same lane for given inputs.
551-
export function findTransitionLane(
552-
lanePriority: LanePriority,
553-
wipLanes: Lanes,
554-
pendingLanes: Lanes,
555-
): Lane {
556-
if (lanePriority === TransitionShortLanePriority) {
557-
// First look for lanes that are completely unclaimed, i.e. have no
558-
// pending work.
559-
let lane = pickArbitraryLane(TransitionShortLanes & ~pendingLanes);
560-
if (lane === NoLane) {
561-
// If all lanes have pending work, look for a lane that isn't currently
562-
// being worked on.
563-
lane = pickArbitraryLane(TransitionShortLanes & ~wipLanes);
564-
if (lane === NoLane) {
565-
// If everything is being worked on, pick any lane. This has the
566-
// effect of interrupting the current work-in-progress.
567-
lane = pickArbitraryLane(TransitionShortLanes);
568-
}
569-
}
570-
return lane;
571-
}
572-
if (lanePriority === TransitionLongLanePriority) {
573-
// First look for lanes that are completely unclaimed, i.e. have no
574-
// pending work.
575-
let lane = pickArbitraryLane(TransitionLongLanes & ~pendingLanes);
531+
export function findTransitionLane(wipLanes: Lanes, pendingLanes: Lanes): Lane {
532+
// First look for lanes that are completely unclaimed, i.e. have no
533+
// pending work.
534+
let lane = pickArbitraryLane(TransitionLanes & ~pendingLanes);
535+
if (lane === NoLane) {
536+
// If all lanes have pending work, look for a lane that isn't currently
537+
// being worked on.
538+
lane = pickArbitraryLane(TransitionLanes & ~wipLanes);
576539
if (lane === NoLane) {
577-
// If all lanes have pending work, look for a lane that isn't currently
578-
// being worked on.
579-
lane = pickArbitraryLane(TransitionLongLanes & ~wipLanes);
580-
if (lane === NoLane) {
581-
// If everything is being worked on, pick any lane. This has the
582-
// effect of interrupting the current work-in-progress.
583-
lane = pickArbitraryLane(TransitionLongLanes);
584-
}
540+
// If everything is being worked on, pick any lane. This has the
541+
// effect of interrupting the current work-in-progress.
542+
lane = pickArbitraryLane(TransitionLanes);
585543
}
586-
return lane;
587544
}
588-
invariant(
589-
false,
590-
'Invalid transition priority: %s. This is a bug in React.',
591-
lanePriority,
592-
);
545+
return lane;
593546
}
594547

595548
// To ensure consistency across multiple updates in the same event, this should
@@ -816,18 +769,14 @@ export function getBumpedLaneForHydration(
816769
case DefaultLanePriority:
817770
lane = DefaultHydrationLane;
818771
break;
819-
case TransitionShortHydrationLanePriority:
820-
case TransitionShortLanePriority:
821-
lane = TransitionShortHydrationLane;
822-
break;
823-
case TransitionLongHydrationLanePriority:
824-
case TransitionLongLanePriority:
825-
lane = TransitionLongHydrationLane;
772+
case TransitionHydrationPriority:
773+
case TransitionPriority:
774+
lane = TransitionHydrationLane;
826775
break;
827776
case RetryLanePriority:
828777
// Shouldn't be reachable under normal circumstances, so there's no
829778
// dedicated lane for retry priority. Use the one for long transitions.
830-
lane = TransitionLongHydrationLane;
779+
lane = TransitionHydrationLane;
831780
break;
832781
case SelectiveHydrationLanePriority:
833782
lane = SelectiveHydrationLane;

‎packages/react-reconciler/src/ReactFiberWorkLoop.new.js

+24-56
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,6 @@ import {
153153
SyncLanePriority,
154154
SyncBatchedLanePriority,
155155
InputDiscreteLanePriority,
156-
TransitionShortLanePriority,
157-
TransitionLongLanePriority,
158156
DefaultLanePriority,
159157
NoLanes,
160158
NoLane,
@@ -457,24 +455,13 @@ export function requestUpdateLane(
457455
// Use the size of the timeout as a heuristic to prioritize shorter
458456
// transitions over longer ones.
459457
// TODO: This will coerce numbers larger than 31 bits to 0.
460-
const timeoutMs = suspenseConfig.timeoutMs;
461-
const transitionLanePriority =
462-
timeoutMs === undefined || (timeoutMs | 0) < 10000
463-
? TransitionShortLanePriority
464-
: TransitionLongLanePriority;
465-
466458
if (currentEventPendingLanes !== NoLanes) {
467459
currentEventPendingLanes =
468460
mostRecentlyUpdatedRoot !== null
469461
? mostRecentlyUpdatedRoot.pendingLanes
470462
: NoLanes;
471463
}
472-
473-
return findTransitionLane(
474-
transitionLanePriority,
475-
currentEventWipLanes,
476-
currentEventPendingLanes,
477-
);
464+
return findTransitionLane(currentEventWipLanes, currentEventPendingLanes);
478465
}
479466

480467
// TODO: Remove this dependency on the Scheduler priority.
@@ -936,60 +923,41 @@ function finishConcurrentRender(root, exitStatus, lanes) {
936923
case RootSuspendedWithDelay: {
937924
markRootSuspended(root, lanes);
938925

939-
if (
940-
// do not delay if we're inside an act() scope
941-
!shouldForceFlushFallbacksInDEV()
942-
) {
943-
// We're suspended in a state that should be avoided. We'll try to
944-
// avoid committing it for as long as the timeouts let us.
945-
const nextLanes = getNextLanes(root, NoLanes);
946-
if (nextLanes !== NoLanes) {
947-
// There's additional work on this root.
948-
break;
949-
}
950-
const suspendedLanes = root.suspendedLanes;
951-
if (!isSubsetOfLanes(suspendedLanes, lanes)) {
952-
// We should prefer to render the fallback of at the last
953-
// suspended level. Ping the last suspended level to try
954-
// rendering it again.
955-
// FIXME: What if the suspended lanes are Idle? Should not restart.
956-
const eventTime = requestEventTime();
957-
markRootPinged(root, suspendedLanes, eventTime);
958-
break;
959-
}
926+
if (workInProgressRootLatestSuspenseTimeout !== NoTimestamp) {
927+
// This is a transition, so we should exit without committing a
928+
// placeholder and without scheduling a timeout. Delay indefinitely
929+
// until we receive more data.
930+
// TODO: Check the lanes to see if it's a transition, instead of
931+
// tracking the latest timeout.
932+
break;
933+
}
934+
935+
if (!shouldForceFlushFallbacksInDEV()) {
936+
// This is not a transition, but we did trigger an avoided state.
937+
// Schedule a placeholder to display after a short delay, using the Just
938+
// Noticable Difference.
939+
// TODO: Is the JND optimization worth the added complexity? If this is
940+
// the only reason we track the event time, then probably not.
941+
// Consider removing.
960942

961943
const mostRecentEventTime = getMostRecentEventTime(root, lanes);
962-
let msUntilTimeout;
963-
if (workInProgressRootLatestSuspenseTimeout !== NoTimestamp) {
964-
// We have processed a suspense config whose expiration time we
965-
// can use as the timeout.
966-
msUntilTimeout = workInProgressRootLatestSuspenseTimeout - now();
967-
} else if (mostRecentEventTime === NoTimestamp) {
968-
// This should never normally happen because only new updates
969-
// cause delayed states, so we should have processed something.
970-
// However, this could also happen in an offscreen tree.
971-
msUntilTimeout = 0;
972-
} else {
973-
// If we didn't process a suspense config, compute a JND based on
974-
// the amount of time elapsed since the most recent event time.
975-
const eventTimeMs = mostRecentEventTime;
976-
const timeElapsedMs = now() - eventTimeMs;
977-
msUntilTimeout = jnd(timeElapsedMs) - timeElapsedMs;
978-
}
944+
const eventTimeMs = mostRecentEventTime;
945+
const timeElapsedMs = now() - eventTimeMs;
946+
const msUntilTimeout = jnd(timeElapsedMs) - timeElapsedMs;
979947

980948
// Don't bother with a very short suspense time.
981949
if (msUntilTimeout > 10) {
982-
// The render is suspended, it hasn't timed out, and there's no
983-
// lower priority work to do. Instead of committing the fallback
984-
// immediately, wait for more data to arrive.
950+
// Instead of committing the fallback immediately, wait for more data
951+
// to arrive.
985952
root.timeoutHandle = scheduleTimeout(
986953
commitRoot.bind(null, root),
987954
msUntilTimeout,
988955
);
989956
break;
990957
}
991958
}
992-
// The work expired. Commit immediately.
959+
960+
// Commit the placeholder.
993961
commitRoot(root);
994962
break;
995963
}

0 commit comments

Comments
 (0)