Skip to content

Commit 62d3f36

Browse files
authored
[Fiber] Trigger default transition indicator if needed (facebook#33160)
Stacked on facebook#33159. This implements `onDefaultTransitionIndicator`. The sequence is: 1) In `markRootUpdated` we schedule Transition updates as needing `indicatorLanes` on the root. This tracks the lanes that currently need an indicator to either start or remain going until this lane commits. 2) Track mutations during any commit. We use the same hook that view transitions use here but instead of tracking it just per view transition scope, we also track a global boolean for the whole root. 3) If a sync/default commit had any mutations, then we clear the indicator lane for the `currentEventTransitionLane`. This requires that the lane is still active while we do these commits. See facebook#33159. In other words, a sync update gets associated with the current transition and it is assumed to be rendering the loading state for that corresponding transition so we don't need a default indicator for this lane. 4) At the end of `processRootScheduleInMicrotask`, right before we're about to enter a new "event transition lane" scope, it is no longer possible to render any more loading states for the current transition lane. That's when we invoke `onDefaultTransitionIndicator` for any roots that have new indicator lanes. 5) When we commit, we remove the finished lanes from `indicatorLanes` and once that reaches zero again, then we can clean up the default indicator. This approach means that you can start multiple different transitions while an indicator is still going but it won't stop/restart each time. Instead, it'll wait until all are done before stopping. Follow ups: - [x] Default updates are currently not enough to cancel because those aren't flush in the same microtask. That's unfortunate. facebook#33186 - [x] Handle async actions before the setState. Since these don't necessarily have a root this is tricky. facebook#33190 - [x] Disable for `useDeferredValue`. ~Since it also goes through `markRootUpdated` and schedules a Transition lane it'll get a default indicator even though it probably shouldn't have one.~ EDIT: Turns out this just works because it doesn't go through `markRootUpdated` when work is left behind. - [x] Implement built-in DOM version by default. facebook#33162
1 parent 0cac32d commit 62d3f36

File tree

9 files changed

+490
-6
lines changed

9 files changed

+490
-6
lines changed

packages/react-noop-renderer/src/createReactNoop.js

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1142,9 +1142,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
11421142
// TODO: Turn this on once tests are fixed
11431143
// console.error(error);
11441144
}
1145-
function onDefaultTransitionIndicator(): void | (() => void) {
1146-
// TODO: Allow this as an option.
1147-
}
1145+
function onDefaultTransitionIndicator(): void | (() => void) {}
11481146

11491147
let idCounter = 0;
11501148

packages/react-reconciler/src/ReactFiberCommitWork.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import type {
2020
import type {Fiber, FiberRoot} from './ReactInternalTypes';
2121
import type {Lanes} from './ReactFiberLane';
2222
import {
23+
includesLoadingIndicatorLanes,
2324
includesOnlySuspenseyCommitEligibleLanes,
2425
includesOnlyViewTransitionEligibleLanes,
2526
} from './ReactFiberLane';
@@ -60,6 +61,7 @@ import {
6061
enableViewTransition,
6162
enableFragmentRefs,
6263
enableEagerAlternateStateNodeCleanup,
64+
enableDefaultTransitionIndicator,
6365
} from 'shared/ReactFeatureFlags';
6466
import {
6567
FunctionComponent,
@@ -268,13 +270,16 @@ import {
268270
} from './ReactFiberCommitViewTransitions';
269271
import {
270272
viewTransitionMutationContext,
273+
pushRootMutationContext,
271274
pushMutationContext,
272275
popMutationContext,
276+
rootMutationContext,
273277
} from './ReactFiberMutationTracking';
274278
import {
275279
trackNamedViewTransition,
276280
untrackNamedViewTransition,
277281
} from './ReactFiberDuplicateViewTransitions';
282+
import {markIndicatorHandled} from './ReactFiberRootScheduler';
278283

279284
// Used during the commit phase to track the state of the Offscreen component stack.
280285
// Allows us to avoid traversing the return path to find the nearest Offscreen ancestor.
@@ -2216,6 +2221,7 @@ function commitMutationEffectsOnFiber(
22162221
case HostRoot: {
22172222
const prevProfilerEffectDuration = pushNestedEffectDurations();
22182223

2224+
pushRootMutationContext();
22192225
if (supportsResources) {
22202226
prepareToCommitHoistables();
22212227

@@ -2265,6 +2271,18 @@ function commitMutationEffectsOnFiber(
22652271
);
22662272
}
22672273

2274+
popMutationContext(false);
2275+
2276+
if (
2277+
enableDefaultTransitionIndicator &&
2278+
rootMutationContext &&
2279+
includesLoadingIndicatorLanes(lanes)
2280+
) {
2281+
// This root had a mutation. Mark this root as having rendered a manual
2282+
// loading state.
2283+
markIndicatorHandled(root);
2284+
}
2285+
22682286
break;
22692287
}
22702288
case HostPortal: {

packages/react-reconciler/src/ReactFiberLane.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
transitionLaneExpirationMs,
2828
retryLaneExpirationMs,
2929
disableLegacyMode,
30+
enableDefaultTransitionIndicator,
3031
} from 'shared/ReactFeatureFlags';
3132
import {isDevToolsPresent} from './ReactFiberDevToolsHook';
3233
import {clz32} from './clz32';
@@ -640,6 +641,10 @@ export function includesOnlySuspenseyCommitEligibleLanes(
640641
);
641642
}
642643

644+
export function includesLoadingIndicatorLanes(lanes: Lanes): boolean {
645+
return (lanes & (SyncLane | DefaultLane)) !== NoLanes;
646+
}
647+
643648
export function includesBlockingLane(lanes: Lanes): boolean {
644649
const SyncDefaultLanes =
645650
InputContinuousHydrationLane |
@@ -766,6 +771,10 @@ export function createLaneMap<T>(initial: T): LaneMap<T> {
766771

767772
export function markRootUpdated(root: FiberRoot, updateLane: Lane) {
768773
root.pendingLanes |= updateLane;
774+
if (enableDefaultTransitionIndicator) {
775+
// Mark that this lane might need a loading indicator to be shown.
776+
root.indicatorLanes |= updateLane & TransitionLanes;
777+
}
769778

770779
// If there are any suspended transitions, it's possible this new update
771780
// could unblock them. Clear the suspended lanes so that we can try rendering
@@ -847,6 +856,10 @@ export function markRootFinished(
847856
root.pingedLanes = NoLanes;
848857
root.warmLanes = NoLanes;
849858

859+
if (enableDefaultTransitionIndicator) {
860+
root.indicatorLanes &= remainingLanes;
861+
}
862+
850863
root.expiredLanes &= remainingLanes;
851864

852865
root.entangledLanes &= remainingLanes;

packages/react-reconciler/src/ReactFiberMutationTracking.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,23 @@
77
* @flow
88
*/
99

10-
import {enableViewTransition} from 'shared/ReactFeatureFlags';
10+
import {
11+
enableDefaultTransitionIndicator,
12+
enableViewTransition,
13+
} from 'shared/ReactFeatureFlags';
1114

15+
export let rootMutationContext: boolean = false;
1216
export let viewTransitionMutationContext: boolean = false;
1317

18+
export function pushRootMutationContext(): void {
19+
if (enableDefaultTransitionIndicator) {
20+
rootMutationContext = false;
21+
}
22+
if (enableViewTransition) {
23+
viewTransitionMutationContext = false;
24+
}
25+
}
26+
1427
export function pushMutationContext(): boolean {
1528
if (!enableViewTransition) {
1629
return false;
@@ -22,12 +35,21 @@ export function pushMutationContext(): boolean {
2235

2336
export function popMutationContext(prev: boolean): void {
2437
if (enableViewTransition) {
38+
if (viewTransitionMutationContext) {
39+
rootMutationContext = true;
40+
}
2541
viewTransitionMutationContext = prev;
2642
}
2743
}
2844

2945
export function trackHostMutation(): void {
46+
// This is extremely hot function that must be inlined. Don't add more stuff.
3047
if (enableViewTransition) {
3148
viewTransitionMutationContext = true;
49+
} else if (enableDefaultTransitionIndicator) {
50+
// We only set this if enableViewTransition is not on. Otherwise we track
51+
// it on the viewTransitionMutationContext and collect it when we pop
52+
// to avoid more than a single operation in this hot path.
53+
rootMutationContext = true;
3254
}
3355
}

packages/react-reconciler/src/ReactFiberRoot.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,9 @@ function FiberRootNode(
7979
this.pingedLanes = NoLanes;
8080
this.warmLanes = NoLanes;
8181
this.expiredLanes = NoLanes;
82+
if (enableDefaultTransitionIndicator) {
83+
this.indicatorLanes = NoLanes;
84+
}
8285
this.errorRecoveryDisabledLanes = NoLanes;
8386
this.shellSuspendCounter = 0;
8487

@@ -94,6 +97,7 @@ function FiberRootNode(
9497

9598
if (enableDefaultTransitionIndicator) {
9699
this.onDefaultTransitionIndicator = onDefaultTransitionIndicator;
100+
this.pendingIndicator = null;
97101
}
98102

99103
this.pooledCache = null;

packages/react-reconciler/src/ReactFiberRootScheduler.js

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
enableComponentPerformanceTrack,
2121
enableYieldingBeforePassive,
2222
enableGestureTransition,
23+
enableDefaultTransitionIndicator,
2324
} from 'shared/ReactFeatureFlags';
2425
import {
2526
NoLane,
@@ -80,6 +81,9 @@ import {
8081
} from './ReactProfilerTimer';
8182
import {peekEntangledActionLane} from './ReactFiberAsyncAction';
8283

84+
import noop from 'shared/noop';
85+
import reportGlobalError from 'shared/reportGlobalError';
86+
8387
// A linked list of all the roots with pending work. In an idiomatic app,
8488
// there's only a single root, but we do support multi root apps, hence this
8589
// extra complexity. But this module is optimized for the single root case.
@@ -316,8 +320,33 @@ function processRootScheduleInMicrotask() {
316320
flushSyncWorkAcrossRoots_impl(syncTransitionLanes, false);
317321
}
318322

319-
// Reset Event Transition Lane so that we allocate a new one next time.
320-
currentEventTransitionLane = NoLane;
323+
if (currentEventTransitionLane !== NoLane) {
324+
// Reset Event Transition Lane so that we allocate a new one next time.
325+
currentEventTransitionLane = NoLane;
326+
startDefaultTransitionIndicatorIfNeeded();
327+
}
328+
}
329+
330+
function startDefaultTransitionIndicatorIfNeeded() {
331+
if (!enableDefaultTransitionIndicator) {
332+
return;
333+
}
334+
// Check all the roots if there are any new indicators needed.
335+
let root = firstScheduledRoot;
336+
while (root !== null) {
337+
if (root.indicatorLanes !== NoLanes && root.pendingIndicator === null) {
338+
// We have new indicator lanes that requires a loading state. Start the
339+
// default transition indicator.
340+
try {
341+
const onDefaultTransitionIndicator = root.onDefaultTransitionIndicator;
342+
root.pendingIndicator = onDefaultTransitionIndicator() || noop;
343+
} catch (x) {
344+
root.pendingIndicator = noop;
345+
reportGlobalError(x);
346+
}
347+
}
348+
root = root.next;
349+
}
321350
}
322351

323352
function scheduleTaskForRootDuringMicrotask(
@@ -664,3 +693,12 @@ export function requestTransitionLane(
664693
export function didCurrentEventScheduleTransition(): boolean {
665694
return currentEventTransitionLane !== NoLane;
666695
}
696+
697+
export function markIndicatorHandled(root: FiberRoot): void {
698+
if (enableDefaultTransitionIndicator) {
699+
// The current transition event rendered a synchronous loading state.
700+
// Clear it from the indicator lanes. We don't need to show a separate
701+
// loading state for this lane.
702+
root.indicatorLanes &= ~currentEventTransitionLane;
703+
}
704+
}

packages/react-reconciler/src/ReactFiberWorkLoop.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,14 @@ import {
5252
enableThrottledScheduling,
5353
enableViewTransition,
5454
enableGestureTransition,
55+
enableDefaultTransitionIndicator,
5556
} from 'shared/ReactFeatureFlags';
5657
import {resetOwnerStackLimit} from 'shared/ReactOwnerStackReset';
5758
import ReactSharedInternals from 'shared/ReactSharedInternals';
5859
import is from 'shared/objectIs';
5960

61+
import reportGlobalError from 'shared/reportGlobalError';
62+
6063
import {
6164
// Aliased because `act` will override and push to an internal queue
6265
scheduleCallback as Scheduler_scheduleCallback,
@@ -3593,6 +3596,33 @@ function flushLayoutEffects(): void {
35933596
const finishedWork = pendingFinishedWork;
35943597
const lanes = pendingEffectsLanes;
35953598

3599+
if (enableDefaultTransitionIndicator) {
3600+
const cleanUpIndicator = root.pendingIndicator;
3601+
if (cleanUpIndicator !== null && root.indicatorLanes === NoLanes) {
3602+
// We have now committed all Transitions that needed the default indicator
3603+
// so we can now run the clean up function. We do this in the layout phase
3604+
// so it has the same semantics as if you did it with a useLayoutEffect or
3605+
// if it was reset automatically with useOptimistic.
3606+
const prevTransition = ReactSharedInternals.T;
3607+
ReactSharedInternals.T = null;
3608+
const previousPriority = getCurrentUpdatePriority();
3609+
setCurrentUpdatePriority(DiscreteEventPriority);
3610+
const prevExecutionContext = executionContext;
3611+
executionContext |= CommitContext;
3612+
root.pendingIndicator = null;
3613+
try {
3614+
cleanUpIndicator();
3615+
} catch (x) {
3616+
reportGlobalError(x);
3617+
} finally {
3618+
// Reset the priority to the previous non-sync value.
3619+
executionContext = prevExecutionContext;
3620+
setCurrentUpdatePriority(previousPriority);
3621+
ReactSharedInternals.T = prevTransition;
3622+
}
3623+
}
3624+
}
3625+
35963626
const subtreeHasLayoutEffects =
35973627
(finishedWork.subtreeFlags & LayoutMask) !== NoFlags;
35983628
const rootHasLayoutEffect = (finishedWork.flags & LayoutMask) !== NoFlags;

packages/react-reconciler/src/ReactInternalTypes.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ type BaseFiberRootProperties = {
248248
pingedLanes: Lanes,
249249
warmLanes: Lanes,
250250
expiredLanes: Lanes,
251+
indicatorLanes: Lanes, // enableDefaultTransitionIndicator only
251252
errorRecoveryDisabledLanes: Lanes,
252253
shellSuspendCounter: number,
253254

@@ -280,7 +281,9 @@ type BaseFiberRootProperties = {
280281
errorInfo: {+componentStack?: ?string},
281282
) => void,
282283

284+
// enableDefaultTransitionIndicator only
283285
onDefaultTransitionIndicator: () => void | (() => void),
286+
pendingIndicator: null | (() => void),
284287

285288
formState: ReactFormState<any, any> | null,
286289

0 commit comments

Comments
 (0)