Skip to content

[Fiber] Trigger default indicator for isomorphic async actions with no root associated #33190

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

Merged
merged 3 commits into from
May 13, 2025
Merged
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
2 changes: 1 addition & 1 deletion fixtures/view-transition/src/components/Page.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@ export default function Page({url, navigate}) {
<button
onClick={() =>
startTransition(async () => {
setShowModal(true);
await sleep(2000);
setShowModal(true);
})
}>
Show Modal
Expand Down
102 changes: 101 additions & 1 deletion packages/react-reconciler/src/ReactFiberAsyncAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import type {
import type {Lane} from './ReactFiberLane';
import type {Transition} from 'react/src/ReactStartTransition';

import {requestTransitionLane} from './ReactFiberRootScheduler';
import {
requestTransitionLane,
ensureScheduleIsScheduled,
} from './ReactFiberRootScheduler';
import {NoLane} from './ReactFiberLane';
import {
hasScheduledTransitionWork,
Expand All @@ -24,9 +27,13 @@ import {
import {
enableComponentPerformanceTrack,
enableProfilerTimer,
enableDefaultTransitionIndicator,
} from 'shared/ReactFeatureFlags';
import {clearEntangledAsyncTransitionTypes} from './ReactFiberTransitionTypes';

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

// If there are multiple, concurrent async actions, they are entangled. All
// transition updates that occur while the async action is still in progress
// are treated as part of the action.
Expand All @@ -46,6 +53,21 @@ let currentEntangledLane: Lane = NoLane;
// until the async action scope has completed.
let currentEntangledActionThenable: Thenable<void> | null = null;

// Track the default indicator for every root. undefined means we haven't
// had any roots registered yet. null means there's more than one callback.
// If there's more than one callback we bailout to not supporting isomorphic
// default indicators.
let isomorphicDefaultTransitionIndicator:
| void
| null
| (() => void | (() => void)) = undefined;
// The clean up function for the currently running indicator.
let pendingIsomorphicIndicator: null | (() => void) = null;
// The number of roots that have pending Transitions that depend on the
// started isomorphic indicator.
let pendingEntangledRoots: number = 0;
let needsIsomorphicIndicator: boolean = false;

export function entangleAsyncAction<S>(
transition: Transition,
thenable: Thenable<S>,
Expand All @@ -66,6 +88,12 @@ export function entangleAsyncAction<S>(
},
};
currentEntangledActionThenable = entangledThenable;
if (enableDefaultTransitionIndicator) {
needsIsomorphicIndicator = true;
// We'll check if we need a default indicator in a microtask. Ensure
// we have this scheduled even if no root is scheduled.
ensureScheduleIsScheduled();
}
}
currentEntangledPendingCount++;
thenable.then(pingEngtangledActionScope, pingEngtangledActionScope);
Expand All @@ -86,6 +114,9 @@ function pingEngtangledActionScope() {
}
}
clearEntangledAsyncTransitionTypes();
if (pendingEntangledRoots === 0) {
stopIsomorphicDefaultIndicator();
}
if (currentEntangledListeners !== null) {
// All the actions have finished. Close the entangled async action scope
// and notify all the listeners.
Expand All @@ -98,6 +129,7 @@ function pingEngtangledActionScope() {
currentEntangledListeners = null;
currentEntangledLane = NoLane;
currentEntangledActionThenable = null;
needsIsomorphicIndicator = false;
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();
Expand Down Expand Up @@ -161,3 +193,71 @@ export function peekEntangledActionLane(): Lane {
export function peekEntangledActionThenable(): Thenable<void> | null {
return currentEntangledActionThenable;
}

export function registerDefaultIndicator(
onDefaultTransitionIndicator: () => void | (() => void),
): void {
if (!enableDefaultTransitionIndicator) {
return;
}
if (isomorphicDefaultTransitionIndicator === undefined) {
isomorphicDefaultTransitionIndicator = onDefaultTransitionIndicator;
} else if (
isomorphicDefaultTransitionIndicator !== onDefaultTransitionIndicator
) {
isomorphicDefaultTransitionIndicator = null;
// Stop any on-going indicator since it's now ambiguous.
stopIsomorphicDefaultIndicator();
}
}

export function startIsomorphicDefaultIndicatorIfNeeded() {
if (!enableDefaultTransitionIndicator) {
return;
}
if (!needsIsomorphicIndicator) {
return;
}
if (
isomorphicDefaultTransitionIndicator != null &&
pendingIsomorphicIndicator === null
) {
try {
pendingIsomorphicIndicator =
isomorphicDefaultTransitionIndicator() || noop;
} catch (x) {
pendingIsomorphicIndicator = noop;
reportGlobalError(x);
}
}
}

function stopIsomorphicDefaultIndicator() {
if (!enableDefaultTransitionIndicator) {
return;
}
if (pendingIsomorphicIndicator !== null) {
const cleanup = pendingIsomorphicIndicator;
pendingIsomorphicIndicator = null;
cleanup();
}
}

function releaseIsomorphicIndicator() {
if (--pendingEntangledRoots === 0) {
stopIsomorphicDefaultIndicator();
}
}

export function hasOngoingIsomorphicIndicator(): boolean {
return pendingIsomorphicIndicator !== null;
}

export function retainIsomorphicIndicator(): () => void {
pendingEntangledRoots++;
return releaseIsomorphicIndicator;
}

export function markIsomorphicIndicatorHandled(): void {
needsIsomorphicIndicator = false;
}
7 changes: 6 additions & 1 deletion packages/react-reconciler/src/ReactFiberReconciler.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ export {
defaultOnRecoverableError,
} from './ReactFiberErrorLogger';
import {getLabelForLane, TotalLanes} from 'react-reconciler/src/ReactFiberLane';
import {registerDefaultIndicator} from './ReactFiberAsyncAction';

type OpaqueRoot = FiberRoot;

Expand Down Expand Up @@ -259,7 +260,7 @@ export function createContainer(
): OpaqueRoot {
const hydrate = false;
const initialChildren = null;
return createFiberRoot(
const root = createFiberRoot(
containerInfo,
tag,
hydrate,
Expand All @@ -274,6 +275,8 @@ export function createContainer(
onDefaultTransitionIndicator,
transitionCallbacks,
);
registerDefaultIndicator(onDefaultTransitionIndicator);
return root;
}

export function createHydrationContainer(
Expand Down Expand Up @@ -323,6 +326,8 @@ export function createHydrationContainer(
transitionCallbacks,
);

registerDefaultIndicator(onDefaultTransitionIndicator);

// TODO: Move this to FiberRoot constructor
root.context = getContextForSubtree(null);

Expand Down
56 changes: 40 additions & 16 deletions packages/react-reconciler/src/ReactFiberRootScheduler.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ import {peekEntangledActionLane} from './ReactFiberAsyncAction';
import noop from 'shared/noop';
import reportGlobalError from 'shared/reportGlobalError';

import {
startIsomorphicDefaultIndicatorIfNeeded,
hasOngoingIsomorphicIndicator,
retainIsomorphicIndicator,
markIsomorphicIndicatorHandled,
} from './ReactFiberAsyncAction';

// 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 @@ -130,6 +137,20 @@ export function ensureRootIsScheduled(root: FiberRoot): void {
// without consulting the schedule.
mightHavePendingSyncWork = true;

ensureScheduleIsScheduled();

if (
__DEV__ &&
!disableLegacyMode &&
ReactSharedInternals.isBatchingLegacy &&
root.tag === LegacyRoot
) {
// Special `act` case: Record whenever a legacy update is scheduled.
ReactSharedInternals.didScheduleLegacyUpdate = true;
}
}

export function ensureScheduleIsScheduled(): void {
// At the end of the current event, go through each of the roots and ensure
// there's a task scheduled for each one at the correct priority.
if (__DEV__ && ReactSharedInternals.actQueue !== null) {
Expand All @@ -144,16 +165,6 @@ export function ensureRootIsScheduled(root: FiberRoot): void {
scheduleImmediateRootScheduleTask();
}
}

if (
__DEV__ &&
!disableLegacyMode &&
ReactSharedInternals.isBatchingLegacy &&
root.tag === LegacyRoot
) {
// Special `act` case: Record whenever a legacy update is scheduled.
ReactSharedInternals.didScheduleLegacyUpdate = true;
}
}

export function flushSyncWorkOnAllRoots() {
Expand Down Expand Up @@ -339,18 +350,30 @@ function startDefaultTransitionIndicatorIfNeeded() {
if (!enableDefaultTransitionIndicator) {
return;
}
// Check if we need to start an isomorphic indicator like if an async action
// was started.
startIsomorphicDefaultIndicatorIfNeeded();
// 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);
if (hasOngoingIsomorphicIndicator()) {
// We already have an isomorphic indicator going which means it has to
// also apply to this root since it implies all roots have the same one.
// We retain this indicator so that it keeps going until we commit this
// root.
root.pendingIndicator = retainIsomorphicIndicator();
} else {
try {
const onDefaultTransitionIndicator =
root.onDefaultTransitionIndicator;
root.pendingIndicator = onDefaultTransitionIndicator() || noop;
} catch (x) {
root.pendingIndicator = noop;
reportGlobalError(x);
}
}
}
root = root.next;
Expand Down Expand Up @@ -708,5 +731,6 @@ export function markIndicatorHandled(root: FiberRoot): void {
// Clear it from the indicator lanes. We don't need to show a separate
// loading state for this lane.
root.indicatorLanes &= ~currentEventTransitionLane;
markIsomorphicIndicatorHandled();
}
}
Loading
Loading