Skip to content

Commit 8da0da0

Browse files
authored
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
awaitact(async()=>{

packages/react-reconciler/src/ReactFiberLane.js

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

46-
exportconstSyncLanePriority: LanePriority=17;
47-
exportconstSyncBatchedLanePriority: LanePriority=16;
46+
exportconstSyncLanePriority: LanePriority=15;
47+
exportconstSyncBatchedLanePriority: LanePriority=14;
4848

49-
constInputDiscreteHydrationLanePriority: LanePriority=15;
50-
exportconstInputDiscreteLanePriority: LanePriority=14;
49+
constInputDiscreteHydrationLanePriority: LanePriority=13;
50+
exportconstInputDiscreteLanePriority: LanePriority=12;
5151

52-
constInputContinuousHydrationLanePriority: LanePriority=13;
53-
exportconstInputContinuousLanePriority: LanePriority=12;
52+
constInputContinuousHydrationLanePriority: LanePriority=11;
53+
exportconstInputContinuousLanePriority: LanePriority=10;
5454

55-
constDefaultHydrationLanePriority: LanePriority=11;
56-
exportconstDefaultLanePriority: LanePriority=10;
55+
constDefaultHydrationLanePriority: LanePriority=9;
56+
exportconstDefaultLanePriority: LanePriority=8;
5757

58-
constTransitionShortHydrationLanePriority: LanePriority=9;
59-
exportconstTransitionShortLanePriority: LanePriority=8;
60-
61-
constTransitionLongHydrationLanePriority: LanePriority=7;
62-
exportconstTransitionLongLanePriority: LanePriority=6;
58+
constTransitionHydrationPriority: LanePriority=7;
59+
exportconstTransitionPriority: LanePriority=6;
6360

6461
constRetryLanePriority: LanePriority=5;
6562

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

92-
constTransitionShortHydrationLane: Lane=/* */0b0000000000000000001000000000000;
93-
constTransitionShortLanes: Lanes=/* */0b0000000000000011110000000000000;
94-
95-
constTransitionLongHydrationLane: Lane=/* */0b0000000000000100000000000000000;
96-
constTransitionLongLanes: Lanes=/* */0b0000000001111000000000000000000;
89+
constTransitionHydrationLane: Lane=/* */0b0000000000000000001000000000000;
90+
constTransitionLanes: Lanes=/* */0b0000000001111111110000000000000;
9791

9892
constRetryLanes: Lanes=/* */0b0000011110000000000000000000000;
9993

@@ -160,23 +154,14 @@ function getHighestPriorityLanes(lanes: Lanes | Lane): Lanes {
160154
return_highestLanePriority =DefaultLanePriority;
161155
returndefaultLanes;
162156
}
163-
if((lanes&TransitionShortHydrationLane)!==NoLanes){
164-
return_highestLanePriority =TransitionShortHydrationLanePriority;
165-
returnTransitionShortHydrationLane;
166-
}
167-
consttransitionShortLanes=TransitionShortLanes&lanes;
168-
if(transitionShortLanes!==NoLanes){
169-
return_highestLanePriority =TransitionShortLanePriority;
170-
returntransitionShortLanes;
157+
if((lanes&TransitionHydrationLane)!==NoLanes){
158+
return_highestLanePriority =TransitionHydrationPriority;
159+
returnTransitionHydrationLane;
171160
}
172-
if((lanes&TransitionLongHydrationLane)!==NoLanes){
173-
return_highestLanePriority =TransitionLongHydrationLanePriority;
174-
returnTransitionLongHydrationLane;
175-
}
176-
consttransitionLongLanes=TransitionLongLanes&lanes;
177-
if(transitionLongLanes!==NoLanes){
178-
return_highestLanePriority =TransitionLongLanePriority;
179-
returntransitionLongLanes;
161+
consttransitionLanes=TransitionLanes&lanes;
162+
if(transitionLanes!==NoLanes){
163+
return_highestLanePriority =TransitionPriority;
164+
returntransitionLanes;
180165
}
181166
constretryLanes=RetryLanes&lanes;
182167
if(retryLanes!==NoLanes){
@@ -241,10 +226,8 @@ export function lanePriorityToSchedulerPriority(
241226
returnUserBlockingSchedulerPriority;
242227
caseDefaultHydrationLanePriority:
243228
caseDefaultLanePriority:
244-
caseTransitionShortHydrationLanePriority:
245-
caseTransitionShortLanePriority:
246-
caseTransitionLongHydrationLanePriority:
247-
caseTransitionLongLanePriority:
229+
caseTransitionHydrationPriority:
230+
caseTransitionPriority:
248231
caseSelectiveHydrationLanePriority:
249232
caseRetryLanePriority:
250233
returnNormalSchedulerPriority;
@@ -402,7 +385,7 @@ function computeExpirationTime(lane: Lane, currentTime: number) {
402385
if(priority>=InputContinuousLanePriority){
403386
// User interactions should expire slightly more quickly.
404387
returncurrentTime+1000;
405-
}elseif(priority>=TransitionLongLanePriority){
388+
}elseif(priority>=TransitionPriority){
406389
returncurrentTime+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
returnlane;
527508
}
528-
caseTransitionShortLanePriority: // Should be handled by findTransitionLane instead
529-
caseTransitionLongLanePriority:
509+
caseTransitionPriority: // Should be handled by findTransitionLane instead
530510
caseRetryLanePriority: // Should be handled by findRetryLane instead
531511
break;
532512
caseIdleLanePriority:
@@ -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-
exportfunctionfindTransitionLane(
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-
letlane=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-
returnlane;
571-
}
572-
if(lanePriority===TransitionLongLanePriority){
573-
// First look for lanes that are completely unclaimed, i.e. have no
574-
// pending work.
575-
letlane=pickArbitraryLane(TransitionLongLanes&~pendingLanes);
531+
exportfunctionfindTransitionLane(wipLanes: Lanes,pendingLanes: Lanes): Lane{
532+
// First look for lanes that are completely unclaimed, i.e. have no
533+
// pending work.
534+
letlane=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-
returnlane;
587544
}
588-
invariant(
589-
false,
590-
'Invalid transition priority: %s. This is a bug in React.',
591-
lanePriority,
592-
);
545+
returnlane;
593546
}
594547

595548
// To ensure consistency across multiple updates in the same event, this should
@@ -816,18 +769,14 @@ export function getBumpedLaneForHydration(
816769
caseDefaultLanePriority:
817770
lane=DefaultHydrationLane;
818771
break;
819-
caseTransitionShortHydrationLanePriority:
820-
caseTransitionShortLanePriority:
821-
lane=TransitionShortHydrationLane;
822-
break;
823-
caseTransitionLongHydrationLanePriority:
824-
caseTransitionLongLanePriority:
825-
lane=TransitionLongHydrationLane;
772+
caseTransitionHydrationPriority:
773+
caseTransitionPriority:
774+
lane=TransitionHydrationLane;
826775
break;
827776
caseRetryLanePriority:
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
caseSelectiveHydrationLanePriority:
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-
consttimeoutMs=suspenseConfig.timeoutMs;
461-
consttransitionLanePriority=
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-
returnfindTransitionLane(
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
caseRootSuspendedWithDelay: {
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-
constnextLanes=getNextLanes(root,NoLanes);
946-
if(nextLanes!==NoLanes){
947-
// There's additional work on this root.
948-
break;
949-
}
950-
constsuspendedLanes=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-
consteventTime=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
constmostRecentEventTime=getMostRecentEventTime(root,lanes);
962-
letmsUntilTimeout;
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-
}elseif(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-
consteventTimeMs=mostRecentEventTime;
976-
consttimeElapsedMs=now()-eventTimeMs;
977-
msUntilTimeout=jnd(timeElapsedMs)-timeElapsedMs;
978-
}
944+
consteventTimeMs=mostRecentEventTime;
945+
consttimeElapsedMs=now()-eventTimeMs;
946+
constmsUntilTimeout=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)
close