Skip to content

[Fiber] Trigger default transition indicator if needed #33160

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions packages/react-noop-renderer/src/createReactNoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -1142,9 +1142,7 @@ function createReactNoop(reconciler: Function, useMutation: boolean) {
// TODO: Turn this on once tests are fixed
// console.error(error);
}
function onDefaultTransitionIndicator(): void | (() => void) {
// TODO: Allow this as an option.
}
function onDefaultTransitionIndicator(): void | (() => void) {}

let idCounter = 0;

Expand Down
18 changes: 18 additions & 0 deletions packages/react-reconciler/src/ReactFiberCommitWork.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {
import type {Fiber, FiberRoot} from './ReactInternalTypes';
import type {Lanes} from './ReactFiberLane';
import {
includesLoadingIndicatorLanes,
includesOnlySuspenseyCommitEligibleLanes,
includesOnlyViewTransitionEligibleLanes,
} from './ReactFiberLane';
Expand Down Expand Up @@ -59,6 +60,7 @@ import {
enableComponentPerformanceTrack,
enableViewTransition,
enableFragmentRefs,
enableDefaultTransitionIndicator,
} from 'shared/ReactFeatureFlags';
import {
FunctionComponent,
Expand Down Expand Up @@ -267,13 +269,16 @@ import {
} from './ReactFiberCommitViewTransitions';
import {
viewTransitionMutationContext,
pushRootMutationContext,
pushMutationContext,
popMutationContext,
rootMutationContext,
} from './ReactFiberMutationTracking';
import {
trackNamedViewTransition,
untrackNamedViewTransition,
} from './ReactFiberDuplicateViewTransitions';
import {markIndicatorHandled} from './ReactFiberRootScheduler';

// Used during the commit phase to track the state of the Offscreen component stack.
// Allows us to avoid traversing the return path to find the nearest Offscreen ancestor.
Expand Down Expand Up @@ -2201,6 +2206,7 @@ function commitMutationEffectsOnFiber(
case HostRoot: {
const prevProfilerEffectDuration = pushNestedEffectDurations();

pushRootMutationContext();
if (supportsResources) {
prepareToCommitHoistables();

Expand Down Expand Up @@ -2250,6 +2256,18 @@ function commitMutationEffectsOnFiber(
);
}

popMutationContext(false);

if (
enableDefaultTransitionIndicator &&
rootMutationContext &&
includesLoadingIndicatorLanes(lanes)
) {
// This root had a mutation. Mark this root as having rendered a manual
// loading state.
markIndicatorHandled(root);
}

break;
}
case HostPortal: {
Expand Down
13 changes: 13 additions & 0 deletions packages/react-reconciler/src/ReactFiberLane.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
transitionLaneExpirationMs,
retryLaneExpirationMs,
disableLegacyMode,
enableDefaultTransitionIndicator,
} from 'shared/ReactFeatureFlags';
import {isDevToolsPresent} from './ReactFiberDevToolsHook';
import {clz32} from './clz32';
Expand Down Expand Up @@ -640,6 +641,10 @@ export function includesOnlySuspenseyCommitEligibleLanes(
);
}

export function includesLoadingIndicatorLanes(lanes: Lanes): boolean {
return (lanes & (SyncLane | DefaultLane)) !== NoLanes;
}

export function includesBlockingLane(lanes: Lanes): boolean {
const SyncDefaultLanes =
InputContinuousHydrationLane |
Expand Down Expand Up @@ -766,6 +771,10 @@ export function createLaneMap<T>(initial: T): LaneMap<T> {

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

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

if (enableDefaultTransitionIndicator) {
root.indicatorLanes &= remainingLanes;
}

root.expiredLanes &= remainingLanes;

root.entangledLanes &= remainingLanes;
Expand Down
24 changes: 23 additions & 1 deletion packages/react-reconciler/src/ReactFiberMutationTracking.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,23 @@
* @flow
*/

import {enableViewTransition} from 'shared/ReactFeatureFlags';
import {
enableDefaultTransitionIndicator,
enableViewTransition,
} from 'shared/ReactFeatureFlags';

export let rootMutationContext: boolean = false;
export let viewTransitionMutationContext: boolean = false;

export function pushRootMutationContext(): void {
if (enableDefaultTransitionIndicator) {
rootMutationContext = false;
}
if (enableViewTransition) {
viewTransitionMutationContext = false;
}
}

export function pushMutationContext(): boolean {
if (!enableViewTransition) {
return false;
Expand All @@ -22,12 +35,21 @@ export function pushMutationContext(): boolean {

export function popMutationContext(prev: boolean): void {
if (enableViewTransition) {
if (viewTransitionMutationContext) {
rootMutationContext = true;
}
viewTransitionMutationContext = prev;
}
}

export function trackHostMutation(): void {
// This is extremely hot function that must be inlined. Don't add more stuff.
if (enableViewTransition) {
viewTransitionMutationContext = true;
} else if (enableDefaultTransitionIndicator) {
// We only set this if enableViewTransition is not on. Otherwise we track
// it on the viewTransitionMutationContext and collect it when we pop
// to avoid more than a single operation in this hot path.
rootMutationContext = true;
}
}
4 changes: 4 additions & 0 deletions packages/react-reconciler/src/ReactFiberRoot.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ function FiberRootNode(
this.pingedLanes = NoLanes;
this.warmLanes = NoLanes;
this.expiredLanes = NoLanes;
if (enableDefaultTransitionIndicator) {
this.indicatorLanes = NoLanes;
}
this.errorRecoveryDisabledLanes = NoLanes;
this.shellSuspendCounter = 0;

Expand All @@ -94,6 +97,7 @@ function FiberRootNode(

if (enableDefaultTransitionIndicator) {
this.onDefaultTransitionIndicator = onDefaultTransitionIndicator;
this.pendingIndicator = null;
}

this.pooledCache = null;
Expand Down
42 changes: 41 additions & 1 deletion packages/react-reconciler/src/ReactFiberRootScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
enableComponentPerformanceTrack,
enableYieldingBeforePassive,
enableGestureTransition,
enableDefaultTransitionIndicator,
} from 'shared/ReactFeatureFlags';
import {
NoLane,
Expand Down Expand Up @@ -79,6 +80,9 @@ import {
syncNestedUpdateFlag,
} from './ReactProfilerTimer';

import noop from 'shared/noop';
import reportGlobalError from 'shared/reportGlobalError';

// A linked list of all the roots with pending work. In an idiomatic app,
// there's only a single root, but we do support multi root apps, hence this
// extra complexity. But this module is optimized for the single root case.
Expand Down Expand Up @@ -257,7 +261,6 @@ function processRootScheduleInMicrotask() {
// preserve the scroll position of the previous page.
syncTransitionLanes = currentEventTransitionLane;
}
currentEventTransitionLane = NoLane;
}

const currentTime = now();
Expand Down Expand Up @@ -315,6 +318,34 @@ function processRootScheduleInMicrotask() {
if (!hasPendingCommitEffects()) {
flushSyncWorkAcrossRoots_impl(syncTransitionLanes, false);
}

if (currentEventTransitionLane !== NoLane) {
// Reset Event Transition Lane so that we allocate a new one next time.
currentEventTransitionLane = NoLane;
startDefaultTransitionIndicatorIfNeeded();
}
}

function startDefaultTransitionIndicatorIfNeeded() {
if (!enableDefaultTransitionIndicator) {
return;
}
// Check all the roots if there are any new indicators needed.
let root = firstScheduledRoot;
while (root !== null) {
if (root.indicatorLanes !== NoLanes && root.pendingIndicator === null) {
// We have new indicator lanes that requires a loading state. Start the
// default transition indicator.
try {
const onDefaultTransitionIndicator = root.onDefaultTransitionIndicator;
root.pendingIndicator = onDefaultTransitionIndicator() || noop;
} catch (x) {
root.pendingIndicator = noop;
reportGlobalError(x);
}
}
root = root.next;
}
}

function scheduleTaskForRootDuringMicrotask(
Expand Down Expand Up @@ -653,3 +684,12 @@ export function requestTransitionLane(
export function didCurrentEventScheduleTransition(): boolean {
return currentEventTransitionLane !== NoLane;
}

export function markIndicatorHandled(root: FiberRoot): void {
if (enableDefaultTransitionIndicator) {
// The current transition event rendered a synchronous loading state.
// Clear it from the indicator lanes. We don't need to show a separate
// loading state for this lane.
root.indicatorLanes &= ~currentEventTransitionLane;
}
}
30 changes: 30 additions & 0 deletions packages/react-reconciler/src/ReactFiberWorkLoop.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,14 @@ import {
enableThrottledScheduling,
enableViewTransition,
enableGestureTransition,
enableDefaultTransitionIndicator,
} from 'shared/ReactFeatureFlags';
import {resetOwnerStackLimit} from 'shared/ReactOwnerStackReset';
import ReactSharedInternals from 'shared/ReactSharedInternals';
import is from 'shared/objectIs';

import reportGlobalError from 'shared/reportGlobalError';

import {
// Aliased because `act` will override and push to an internal queue
scheduleCallback as Scheduler_scheduleCallback,
Expand Down Expand Up @@ -3601,6 +3604,33 @@ function flushLayoutEffects(): void {
const finishedWork = pendingFinishedWork;
const lanes = pendingEffectsLanes;

if (enableDefaultTransitionIndicator) {
const cleanUpIndicator = root.pendingIndicator;
if (cleanUpIndicator !== null && root.indicatorLanes === NoLanes) {
// We have now committed all Transitions that needed the default indicator
// so we can now run the clean up function. We do this in the layout phase
// so it has the same semantics as if you did it with a useLayoutEffect or
// if it was reset automatically with useOptimistic.
const prevTransition = ReactSharedInternals.T;
ReactSharedInternals.T = null;
const previousPriority = getCurrentUpdatePriority();
setCurrentUpdatePriority(DiscreteEventPriority);
const prevExecutionContext = executionContext;
executionContext |= CommitContext;
root.pendingIndicator = null;
try {
cleanUpIndicator();
} catch (x) {
reportGlobalError(x);
} finally {
// Reset the priority to the previous non-sync value.
executionContext = prevExecutionContext;
setCurrentUpdatePriority(previousPriority);
ReactSharedInternals.T = prevTransition;
}
}
}

const subtreeHasLayoutEffects =
(finishedWork.subtreeFlags & LayoutMask) !== NoFlags;
const rootHasLayoutEffect = (finishedWork.flags & LayoutMask) !== NoFlags;
Expand Down
3 changes: 3 additions & 0 deletions packages/react-reconciler/src/ReactInternalTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ type BaseFiberRootProperties = {
pingedLanes: Lanes,
warmLanes: Lanes,
expiredLanes: Lanes,
indicatorLanes: Lanes, // enableDefaultTransitionIndicator only
errorRecoveryDisabledLanes: Lanes,
shellSuspendCounter: number,

Expand Down Expand Up @@ -280,7 +281,9 @@ type BaseFiberRootProperties = {
errorInfo: {+componentStack?: ?string},
) => void,

// enableDefaultTransitionIndicator only
onDefaultTransitionIndicator: () => void | (() => void),
pendingIndicator: null | (() => void),

formState: ReactFormState<any, any> | null,

Expand Down
Loading
Loading