Skip to content

Commit 3a5b326

Browse files
authored
[Fiber] Trigger default indicator for isomorphic async actions with no root associated (facebook#33190)
Stacked on facebook#33160, facebook#33162, facebook#33186 and facebook#33188. We have a special case that's awkward for default indicators. When you start a new async Transition from `React.startTransition` then there's not yet any associated root with the Transition because you haven't necessarily `setState` on anything yet until the promise resolves. That's what `entangleAsyncAction` handles by creating a lane that everything entangles with until all async actions are done. If there are no sync updates before the end of the event, we should trigger a default indicator until either the async action completes without update or if it gets entangled with some roots we should keep it going until those roots are done.
1 parent 5944042 commit 3a5b326

File tree

5 files changed

+274
-20
lines changed

5 files changed

+274
-20
lines changed

fixtures/view-transition/src/components/Page.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,8 @@ export default function Page({url, navigate}) {
113113
<button
114114
onClick={() =>
115115
startTransition(async () => {
116-
setShowModal(true);
117116
await sleep(2000);
117+
setShowModal(true);
118118
})
119119
}>
120120
Show Modal

packages/react-reconciler/src/ReactFiberAsyncAction.js

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ import type {
1515
import type {Lane} from './ReactFiberLane';
1616
import type {Transition} from 'react/src/ReactStartTransition';
1717

18-
import {requestTransitionLane} from './ReactFiberRootScheduler';
18+
import {
19+
requestTransitionLane,
20+
ensureScheduleIsScheduled,
21+
} from './ReactFiberRootScheduler';
1922
import {NoLane} from './ReactFiberLane';
2023
import {
2124
hasScheduledTransitionWork,
@@ -24,9 +27,13 @@ import {
2427
import {
2528
enableComponentPerformanceTrack,
2629
enableProfilerTimer,
30+
enableDefaultTransitionIndicator,
2731
} from 'shared/ReactFeatureFlags';
2832
import {clearEntangledAsyncTransitionTypes} from './ReactFiberTransitionTypes';
2933

34+
import noop from 'shared/noop';
35+
import reportGlobalError from 'shared/reportGlobalError';
36+
3037
// If there are multiple, concurrent async actions, they are entangled. All
3138
// transition updates that occur while the async action is still in progress
3239
// are treated as part of the action.
@@ -46,6 +53,21 @@ let currentEntangledLane: Lane = NoLane;
4653
// until the async action scope has completed.
4754
let currentEntangledActionThenable: Thenable<void> | null = null;
4855

56+
// Track the default indicator for every root. undefined means we haven't
57+
// had any roots registered yet. null means there's more than one callback.
58+
// If there's more than one callback we bailout to not supporting isomorphic
59+
// default indicators.
60+
let isomorphicDefaultTransitionIndicator:
61+
| void
62+
| null
63+
| (() => void | (() => void)) = undefined;
64+
// The clean up function for the currently running indicator.
65+
let pendingIsomorphicIndicator: null | (() => void) = null;
66+
// The number of roots that have pending Transitions that depend on the
67+
// started isomorphic indicator.
68+
let pendingEntangledRoots: number = 0;
69+
let needsIsomorphicIndicator: boolean = false;
70+
4971
export function entangleAsyncAction<S>(
5072
transition: Transition,
5173
thenable: Thenable<S>,
@@ -66,6 +88,12 @@ export function entangleAsyncAction<S>(
6688
},
6789
};
6890
currentEntangledActionThenable = entangledThenable;
91+
if (enableDefaultTransitionIndicator) {
92+
needsIsomorphicIndicator = true;
93+
// We'll check if we need a default indicator in a microtask. Ensure
94+
// we have this scheduled even if no root is scheduled.
95+
ensureScheduleIsScheduled();
96+
}
6997
}
7098
currentEntangledPendingCount++;
7199
thenable.then(pingEngtangledActionScope, pingEngtangledActionScope);
@@ -86,6 +114,9 @@ function pingEngtangledActionScope() {
86114
}
87115
}
88116
clearEntangledAsyncTransitionTypes();
117+
if (pendingEntangledRoots === 0) {
118+
stopIsomorphicDefaultIndicator();
119+
}
89120
if (currentEntangledListeners !== null) {
90121
// All the actions have finished. Close the entangled async action scope
91122
// and notify all the listeners.
@@ -98,6 +129,7 @@ function pingEngtangledActionScope() {
98129
currentEntangledListeners = null;
99130
currentEntangledLane = NoLane;
100131
currentEntangledActionThenable = null;
132+
needsIsomorphicIndicator = false;
101133
for (let i = 0; i < listeners.length; i++) {
102134
const listener = listeners[i];
103135
listener();
@@ -161,3 +193,71 @@ export function peekEntangledActionLane(): Lane {
161193
export function peekEntangledActionThenable(): Thenable<void> | null {
162194
return currentEntangledActionThenable;
163195
}
196+
197+
export function registerDefaultIndicator(
198+
onDefaultTransitionIndicator: () => void | (() => void),
199+
): void {
200+
if (!enableDefaultTransitionIndicator) {
201+
return;
202+
}
203+
if (isomorphicDefaultTransitionIndicator === undefined) {
204+
isomorphicDefaultTransitionIndicator = onDefaultTransitionIndicator;
205+
} else if (
206+
isomorphicDefaultTransitionIndicator !== onDefaultTransitionIndicator
207+
) {
208+
isomorphicDefaultTransitionIndicator = null;
209+
// Stop any on-going indicator since it's now ambiguous.
210+
stopIsomorphicDefaultIndicator();
211+
}
212+
}
213+
214+
export function startIsomorphicDefaultIndicatorIfNeeded() {
215+
if (!enableDefaultTransitionIndicator) {
216+
return;
217+
}
218+
if (!needsIsomorphicIndicator) {
219+
return;
220+
}
221+
if (
222+
isomorphicDefaultTransitionIndicator != null &&
223+
pendingIsomorphicIndicator === null
224+
) {
225+
try {
226+
pendingIsomorphicIndicator =
227+
isomorphicDefaultTransitionIndicator() || noop;
228+
} catch (x) {
229+
pendingIsomorphicIndicator = noop;
230+
reportGlobalError(x);
231+
}
232+
}
233+
}
234+
235+
function stopIsomorphicDefaultIndicator() {
236+
if (!enableDefaultTransitionIndicator) {
237+
return;
238+
}
239+
if (pendingIsomorphicIndicator !== null) {
240+
const cleanup = pendingIsomorphicIndicator;
241+
pendingIsomorphicIndicator = null;
242+
cleanup();
243+
}
244+
}
245+
246+
function releaseIsomorphicIndicator() {
247+
if (--pendingEntangledRoots === 0) {
248+
stopIsomorphicDefaultIndicator();
249+
}
250+
}
251+
252+
export function hasOngoingIsomorphicIndicator(): boolean {
253+
return pendingIsomorphicIndicator !== null;
254+
}
255+
256+
export function retainIsomorphicIndicator(): () => void {
257+
pendingEntangledRoots++;
258+
return releaseIsomorphicIndicator;
259+
}
260+
261+
export function markIsomorphicIndicatorHandled(): void {
262+
needsIsomorphicIndicator = false;
263+
}

packages/react-reconciler/src/ReactFiberReconciler.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ export {
125125
defaultOnRecoverableError,
126126
} from './ReactFiberErrorLogger';
127127
import {getLabelForLane, TotalLanes} from 'react-reconciler/src/ReactFiberLane';
128+
import {registerDefaultIndicator} from './ReactFiberAsyncAction';
128129

129130
type OpaqueRoot = FiberRoot;
130131

@@ -259,7 +260,7 @@ export function createContainer(
259260
): OpaqueRoot {
260261
const hydrate = false;
261262
const initialChildren = null;
262-
return createFiberRoot(
263+
const root = createFiberRoot(
263264
containerInfo,
264265
tag,
265266
hydrate,
@@ -274,6 +275,8 @@ export function createContainer(
274275
onDefaultTransitionIndicator,
275276
transitionCallbacks,
276277
);
278+
registerDefaultIndicator(onDefaultTransitionIndicator);
279+
return root;
277280
}
278281

279282
export function createHydrationContainer(
@@ -323,6 +326,8 @@ export function createHydrationContainer(
323326
transitionCallbacks,
324327
);
325328

329+
registerDefaultIndicator(onDefaultTransitionIndicator);
330+
326331
// TODO: Move this to FiberRoot constructor
327332
root.context = getContextForSubtree(null);
328333

packages/react-reconciler/src/ReactFiberRootScheduler.js

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,13 @@ import {peekEntangledActionLane} from './ReactFiberAsyncAction';
8585
import noop from 'shared/noop';
8686
import reportGlobalError from 'shared/reportGlobalError';
8787

88+
import {
89+
startIsomorphicDefaultIndicatorIfNeeded,
90+
hasOngoingIsomorphicIndicator,
91+
retainIsomorphicIndicator,
92+
markIsomorphicIndicatorHandled,
93+
} from './ReactFiberAsyncAction';
94+
8895
// A linked list of all the roots with pending work. In an idiomatic app,
8996
// there's only a single root, but we do support multi root apps, hence this
9097
// extra complexity. But this module is optimized for the single root case.
@@ -130,6 +137,20 @@ export function ensureRootIsScheduled(root: FiberRoot): void {
130137
// without consulting the schedule.
131138
mightHavePendingSyncWork = true;
132139

140+
ensureScheduleIsScheduled();
141+
142+
if (
143+
__DEV__ &&
144+
!disableLegacyMode &&
145+
ReactSharedInternals.isBatchingLegacy &&
146+
root.tag === LegacyRoot
147+
) {
148+
// Special `act` case: Record whenever a legacy update is scheduled.
149+
ReactSharedInternals.didScheduleLegacyUpdate = true;
150+
}
151+
}
152+
153+
export function ensureScheduleIsScheduled(): void {
133154
// At the end of the current event, go through each of the roots and ensure
134155
// there's a task scheduled for each one at the correct priority.
135156
if (__DEV__ && ReactSharedInternals.actQueue !== null) {
@@ -144,16 +165,6 @@ export function ensureRootIsScheduled(root: FiberRoot): void {
144165
scheduleImmediateRootScheduleTask();
145166
}
146167
}
147-
148-
if (
149-
__DEV__ &&
150-
!disableLegacyMode &&
151-
ReactSharedInternals.isBatchingLegacy &&
152-
root.tag === LegacyRoot
153-
) {
154-
// Special `act` case: Record whenever a legacy update is scheduled.
155-
ReactSharedInternals.didScheduleLegacyUpdate = true;
156-
}
157168
}
158169

159170
export function flushSyncWorkOnAllRoots() {
@@ -339,18 +350,30 @@ function startDefaultTransitionIndicatorIfNeeded() {
339350
if (!enableDefaultTransitionIndicator) {
340351
return;
341352
}
353+
// Check if we need to start an isomorphic indicator like if an async action
354+
// was started.
355+
startIsomorphicDefaultIndicatorIfNeeded();
342356
// Check all the roots if there are any new indicators needed.
343357
let root = firstScheduledRoot;
344358
while (root !== null) {
345359
if (root.indicatorLanes !== NoLanes && root.pendingIndicator === null) {
346360
// We have new indicator lanes that requires a loading state. Start the
347361
// default transition indicator.
348-
try {
349-
const onDefaultTransitionIndicator = root.onDefaultTransitionIndicator;
350-
root.pendingIndicator = onDefaultTransitionIndicator() || noop;
351-
} catch (x) {
352-
root.pendingIndicator = noop;
353-
reportGlobalError(x);
362+
if (hasOngoingIsomorphicIndicator()) {
363+
// We already have an isomorphic indicator going which means it has to
364+
// also apply to this root since it implies all roots have the same one.
365+
// We retain this indicator so that it keeps going until we commit this
366+
// root.
367+
root.pendingIndicator = retainIsomorphicIndicator();
368+
} else {
369+
try {
370+
const onDefaultTransitionIndicator =
371+
root.onDefaultTransitionIndicator;
372+
root.pendingIndicator = onDefaultTransitionIndicator() || noop;
373+
} catch (x) {
374+
root.pendingIndicator = noop;
375+
reportGlobalError(x);
376+
}
354377
}
355378
}
356379
root = root.next;
@@ -708,5 +731,6 @@ export function markIndicatorHandled(root: FiberRoot): void {
708731
// Clear it from the indicator lanes. We don't need to show a separate
709732
// loading state for this lane.
710733
root.indicatorLanes &= ~currentEventTransitionLane;
734+
markIsomorphicIndicatorHandled();
711735
}
712736
}

0 commit comments

Comments
 (0)