Skip to content

Commit fe9d199

Browse files
committed
Prevent intermediate transitions from finishing
When multiple transitions update the same queue, only the most recent one should be allowed to finish. Do not display intermediate states. For example, if you click on multiple tabs in quick succession, we should not switch to any tab that isn't the last one you clicked.
1 parent 772e7c1 commit fe9d199

11 files changed

Lines changed: 522 additions & 61 deletions

packages/react-reconciler/src/ReactFiberHooks.js

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import {
5656
NormalPriority,
5757
} from './SchedulerWithReactIntegration';
5858
import {requestCurrentSuspenseConfig} from './ReactFiberSuspenseConfig';
59+
import {preventIntermediateStates} from 'shared/ReactFeatureFlags';
5960

6061
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
6162

@@ -221,6 +222,9 @@ let currentTransitionEventTime: ExpirationTime = NoWork;
221222
let currentTransitionPendingTime: ExpirationTime = NoWork;
222223
// The expiration time of the current transition.
223224
let currentTransitionResolvedTime: ExpirationTime = NoWork;
225+
// The expiration time of the transitions that were superseded by the current
226+
// transition. This is accumulated during `startTransition`.
227+
let currentTransitionSupersededTime: ExpirationTime = NoWork;
224228

225229
function mountHookTypesDev() {
226230
if (__DEV__) {
@@ -1522,7 +1526,7 @@ function startTransition(
15221526
},
15231527
);
15241528

1525-
scheduleUpdateOnFiber(fiber, currentTransitionPendingTime);
1529+
const root = scheduleUpdateOnFiber(fiber, currentTransitionPendingTime);
15261530

15271531
runWithPriority(
15281532
priorityLevel > NormalPriority ? NormalPriority : priorityLevel,
@@ -1547,6 +1551,18 @@ function startTransition(
15471551
transitionInstance.resolvedTime = currentTransitionResolvedTime;
15481552
transitionInstance.version++;
15491553

1554+
if (
1555+
preventIntermediateStates &&
1556+
currentTransitionSupersededTime !== NoWork &&
1557+
root !== null
1558+
) {
1559+
markTransitionRange(
1560+
root,
1561+
currentTransitionSupersededTime,
1562+
currentTransitionResolvedTime,
1563+
);
1564+
}
1565+
15501566
ReactCurrentBatchConfig.suspense = previousConfig;
15511567
currentTransition = previousTransition;
15521568
}
@@ -1577,6 +1593,15 @@ export function setTransition(
15771593
// There's already a pending transition on this queue. The new transition
15781594
// supersedes the old one.
15791595

1596+
// Track the expiration time of the superseded transition. If there are
1597+
// multiple, choose the highest priority one.
1598+
if (preventIntermediateStates) {
1599+
const resolvedTime = prevTransition.resolvedTime;
1600+
if (currentTransitionSupersededTime < resolvedTime) {
1601+
currentTransitionSupersededTime = resolvedTime;
1602+
}
1603+
}
1604+
15801605
// Turn off the `isPending` state of the previous transition, at the same
15811606
// priority we use to turn on the `isPending` state of the
15821607
// current transition.
@@ -1587,6 +1612,41 @@ export function setTransition(
15871612
}
15881613
}
15891614

1615+
function markTransitionRange(root, start, end) {
1616+
// This transition supersedes one or more previous ones. We must prevent the
1617+
// earlier transitions from committing independently of the new ones. Track
1618+
// the ranges of expiration times that must commit in a single batch. These
1619+
// will be removed when the root finishes.
1620+
const pendingRanges = root.pendingRanges;
1621+
if (pendingRanges === null) {
1622+
root.pendingRanges = [start, end];
1623+
} else {
1624+
// Check if the new range overlaps with an existing range. No two ranges
1625+
// should ever overlap.
1626+
for (let i = 0; i < pendingRanges.length; ) {
1627+
const start2 = pendingRanges[i];
1628+
const end2 = pendingRanges[i + 1];
1629+
if (start >= end2 && end <= start2) {
1630+
// Found an overlapping range. Combine them.
1631+
start = start > start2 ? start : start2;
1632+
end = end < end2 ? end : end2;
1633+
// There could be multiple overlapping ranges. Remove this one and keep
1634+
// iterating. We'll add a single, combined range at the end.
1635+
pendingRanges.splice(i, 2);
1636+
// Continue searching for overlapping ranges. We don't need to check the
1637+
// ranges from earlier in the list -- we know they don't overlap with
1638+
// either of the ranges we just combined, so it follows they don't
1639+
// overlap with the combined range.
1640+
} else {
1641+
// Only increment if we didn't remove the range in the block above.
1642+
i += 2;
1643+
}
1644+
}
1645+
// Add the range to the list.
1646+
pendingRanges.push(start, end);
1647+
}
1648+
}
1649+
15901650
export const ContextOnlyDispatcher: Dispatcher = {
15911651
readContext,
15921652

packages/react-reconciler/src/ReactFiberRoot.js

Lines changed: 129 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@ import type {ReactPriorityLevel} from './SchedulerWithReactIntegration';
1818

1919
import {noTimeout} from './ReactFiberHostConfig';
2020
import {createHostRootFiber} from './ReactFiber';
21-
import {NoWork} from './ReactFiberExpirationTime';
21+
import {NoWork, Idle} from './ReactFiberExpirationTime';
2222
import {
2323
enableSchedulerTracing,
2424
enableSuspenseCallback,
25+
enableTrainModelFix,
2526
} from 'shared/ReactFeatureFlags';
2627
import {unstable_getThreadID} from 'scheduler/tracing';
2728
import {NoPriority} from './SchedulerWithReactIntegration';
@@ -70,6 +71,12 @@ type BaseFiberRootProperties = {|
7071
lastSuspendedTime: ExpirationTime,
7172
// The next known expiration time after the suspended range
7273
nextKnownPendingLevel: ExpirationTime,
74+
// Ranges of expiration times each of which must be committed as a single
75+
// batch. Any update that is part of a range must commit at the same time as
76+
// all of the other updates in that range. Ranges do not overlap.
77+
//
78+
// For ranges 1..n, structure is [start1, end1, start2, end2...startN, endN]
79+
pendingRanges: Array<ExpirationTime> | null,
7380
// The latest time at which a suspended component pinged the root to
7481
// render again
7582
lastPingedTime: ExpirationTime,
@@ -121,6 +128,7 @@ function FiberRootNode(containerInfo, tag, hydrate) {
121128
this.firstSuspendedTime = NoWork;
122129
this.lastSuspendedTime = NoWork;
123130
this.nextKnownPendingLevel = NoWork;
131+
this.pendingRanges = null;
124132
this.lastPingedTime = NoWork;
125133
this.lastExpiredTime = NoWork;
126134

@@ -156,17 +164,79 @@ export function createFiberRoot(
156164
return root;
157165
}
158166

159-
export function isRootSuspendedAtTime(
167+
export function getNextRootExpirationTimeToWorkOn(
160168
root: FiberRoot,
161-
expirationTime: ExpirationTime,
162-
): boolean {
169+
): ExpirationTime {
170+
// Determines the next expiration time that the root should render, taking
171+
// into account levels that may be suspended, or levels that may have
172+
// received a ping.
173+
const lastExpiredTime = resolveTransitionTime(root, root.lastExpiredTime);
174+
if (lastExpiredTime !== NoWork) {
175+
return lastExpiredTime;
176+
}
177+
178+
// Check if the root is suspended. "Pending" refers to any update that hasn't
179+
// committed yet, including if it suspended. The "suspended" range is
180+
// therefore a subset.
181+
const firstPendingTime = resolveTransitionTime(root, root.firstPendingTime);
163182
const firstSuspendedTime = root.firstSuspendedTime;
164183
const lastSuspendedTime = root.lastSuspendedTime;
165-
return (
166-
firstSuspendedTime !== NoWork &&
167-
firstSuspendedTime >= expirationTime &&
168-
lastSuspendedTime <= expirationTime
184+
if (
185+
!(
186+
firstSuspendedTime !== NoWork &&
187+
firstSuspendedTime >= firstPendingTime &&
188+
lastSuspendedTime <= firstPendingTime
189+
)
190+
) {
191+
// The highest priority pending time is not suspended. Let's work on that.
192+
return firstPendingTime;
193+
}
194+
195+
// If the first pending time is suspended, check if there's a lower priority
196+
// pending level that we know about. Or check if we received a ping. Work
197+
// on whichever is higher priority.
198+
const lastPingedTime = root.lastPingedTime;
199+
const nextKnownPendingLevel = root.nextKnownPendingLevel;
200+
const nextLevel = resolveTransitionTime(
201+
root,
202+
lastPingedTime > nextKnownPendingLevel
203+
? lastPingedTime
204+
: nextKnownPendingLevel,
169205
);
206+
if (
207+
enableTrainModelFix &&
208+
nextLevel <= Idle &&
209+
firstPendingTime !== nextLevel
210+
) {
211+
// Don't work on Idle/Never priority unless everything else is committed.
212+
return NoWork;
213+
}
214+
return nextLevel;
215+
}
216+
217+
function resolveTransitionTime(root, expirationTime) {
218+
if (expirationTime === NoWork) {
219+
return NoWork;
220+
}
221+
const pendingRanges = root.pendingRanges;
222+
if (pendingRanges !== null) {
223+
// Check if the expiration time is part of a transition range. If so, resolve
224+
// it to the end of that range, since that will encompass all the times in the
225+
// entire range. A single pass is sufficient because the ranges do
226+
// not overlap.
227+
let resolvedTime = expirationTime;
228+
for (let i = 0; i < pendingRanges.length; i += 2) {
229+
const start = pendingRanges[i];
230+
const end = pendingRanges[i + 1];
231+
if (start >= resolvedTime) {
232+
if (resolvedTime > end) {
233+
resolvedTime = end;
234+
}
235+
}
236+
}
237+
return resolvedTime;
238+
}
239+
return expirationTime;
170240
}
171241

172242
export function markRootSuspendedAtTime(
@@ -191,6 +261,42 @@ export function markRootSuspendedAtTime(
191261
}
192262
}
193263

264+
export function markRootPingedAtTime(
265+
root: FiberRoot,
266+
expirationTime: ExpirationTime,
267+
): void {
268+
const lastPingedTime = root.lastPingedTime;
269+
if (enableTrainModelFix) {
270+
// Track the lowest priority ping that isn't at Idle priority. Unless
271+
// there are *only* Idle pings. Any non-Idle ping beats an Idle ping.
272+
if (expirationTime <= Idle) {
273+
// This is an Idle ping.
274+
if (
275+
lastPingedTime === NoWork ||
276+
(lastPingedTime <= Idle && lastPingedTime > expirationTime)
277+
) {
278+
// There are only Idle pings.
279+
root.lastPingedTime = expirationTime;
280+
}
281+
} else {
282+
// This is a non-Idle ping.
283+
if (
284+
lastPingedTime === NoWork ||
285+
lastPingedTime > expirationTime ||
286+
lastPingedTime <= Idle
287+
) {
288+
// This is the lowest priority non-Idle ping.
289+
root.lastPingedTime = expirationTime;
290+
}
291+
}
292+
} else {
293+
// Don't special case Idle pings.
294+
if (lastPingedTime === NoWork || lastPingedTime > expirationTime) {
295+
root.lastPingedTime = expirationTime;
296+
}
297+
}
298+
}
299+
194300
export function markRootUpdatedAtTime(
195301
root: FiberRoot,
196302
expirationTime: ExpirationTime,
@@ -249,6 +355,21 @@ export function markRootFinishedAtTime(
249355
// Clear the expired time
250356
root.lastExpiredTime = NoWork;
251357
}
358+
359+
// Clear pending transition ranges
360+
const pendingRanges = root.pendingRanges;
361+
if (pendingRanges !== null) {
362+
for (let i = 0; i < pendingRanges.length; ) {
363+
const end = pendingRanges[i + 1];
364+
if (finishedExpirationTime <= end) {
365+
// Remove this range from the set
366+
pendingRanges.splice(i, 2);
367+
} else {
368+
// Only increment if we didn't remove the range in the block above.
369+
i += 2;
370+
}
371+
}
372+
}
252373
}
253374

254375
export function markRootExpiredAtTime(

packages/react-reconciler/src/ReactFiberWorkLoop.js

Lines changed: 3 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,12 @@ import {
6565

6666
import {createWorkInProgress, assignFiberPropertiesInDEV} from './ReactFiber';
6767
import {
68-
isRootSuspendedAtTime,
6968
markRootSuspendedAtTime,
7069
markRootFinishedAtTime,
7170
markRootUpdatedAtTime,
7271
markRootExpiredAtTime,
72+
getNextRootExpirationTimeToWorkOn,
73+
markRootPingedAtTime,
7374
} from './ReactFiberRoot';
7475
import {
7576
NoMode,
@@ -517,44 +518,6 @@ function markUpdateTimeFromFiberToRoot(fiber, expirationTime) {
517518
return root;
518519
}
519520

520-
function getNextRootExpirationTimeToWorkOn(root: FiberRoot): ExpirationTime {
521-
// Determines the next expiration time that the root should render, taking
522-
// into account levels that may be suspended, or levels that may have
523-
// received a ping.
524-
525-
const lastExpiredTime = root.lastExpiredTime;
526-
if (lastExpiredTime !== NoWork) {
527-
return lastExpiredTime;
528-
}
529-
530-
// "Pending" refers to any update that hasn't committed yet, including if it
531-
// suspended. The "suspended" range is therefore a subset.
532-
const firstPendingTime = root.firstPendingTime;
533-
if (!isRootSuspendedAtTime(root, firstPendingTime)) {
534-
// The highest priority pending time is not suspended. Let's work on that.
535-
return firstPendingTime;
536-
}
537-
538-
// If the first pending time is suspended, check if there's a lower priority
539-
// pending level that we know about. Or check if we received a ping. Work
540-
// on whichever is higher priority.
541-
const lastPingedTime = root.lastPingedTime;
542-
const nextKnownPendingLevel = root.nextKnownPendingLevel;
543-
const nextLevel =
544-
lastPingedTime > nextKnownPendingLevel
545-
? lastPingedTime
546-
: nextKnownPendingLevel;
547-
if (
548-
enableTrainModelFix &&
549-
nextLevel <= Idle &&
550-
firstPendingTime !== nextLevel
551-
) {
552-
// Don't work on Idle/Never priority unless everything else is committed.
553-
return NoWork;
554-
}
555-
return nextLevel;
556-
}
557-
558521
// Use this function to schedule a task for a root. There's only one task per
559522
// root; if a task was already scheduled, we'll check to make sure the
560523
// expiration time of the existing task is the same as the expiration time of
@@ -2331,19 +2294,7 @@ export function pingSuspendedRoot(
23312294
return;
23322295
}
23332296

2334-
if (!isRootSuspendedAtTime(root, suspendedTime)) {
2335-
// The root is no longer suspended at this time.
2336-
return;
2337-
}
2338-
2339-
const lastPingedTime = root.lastPingedTime;
2340-
if (lastPingedTime !== NoWork && lastPingedTime < suspendedTime) {
2341-
// There's already a lower priority ping scheduled.
2342-
return;
2343-
}
2344-
2345-
// Mark the time at which this ping was scheduled.
2346-
root.lastPingedTime = suspendedTime;
2297+
markRootPingedAtTime(root, suspendedTime);
23472298

23482299
if (!enableTrainModelFix && root.finishedExpirationTime === suspendedTime) {
23492300
// If there's a pending fallback waiting to commit, throw it away.

0 commit comments

Comments
 (0)