Skip to content

Commit 731ae3e

Browse files
authored
Solidify addTransitionType Semantics (facebook#32797)
Stacked on facebook#32793. This is meant to model the intended semantics of `addTransitionType` better. The previous hack just consumed all transition types when any root committed so it could steal them from other roots. Really each root should get its own set. Really each transition lane should get its own set. We can't implement the full ideal semantics yet because 1) we currently entangle transition lanes 2) we lack `AsyncContext` on the client so for async actions we can't associate a `addTransitionType` call to a specific `startTransition`. This starts by modeling Transition Types to be stored on the Transition instance. Conceptually they belong to the Transition instance of that `startTransition` they belong to. That instance is otherwise mostly just used for Transition Tracing but it makes sense that those would be able to be passed the Transition Types for that specific instance. Nested `startTransition` need to get entangled. So that this `addTransitionType` can be associated with the `setState`: ```js startTransition(() => { startTransition(() => { addTransitionType(...) }); setState(...); }); ``` Ideally we'd probably just use the same Transition instance itself since these are conceptually all part of one entangled one. But transition tracing uses multiple names and start times. Unclear what we want to do with that. So I kept separate instances but shared `types` set. Next I collect the types added during a `startTransition` to any root scheduled with a Transition. This should really be collected one set per Transition lane in a `LaneMap`. In fact, the information would already be there if Transition Tracing was always enabled because it tracks all Transition instances per lane. For now I just keep track of one set for all Transition lanes. Maybe we should only add it if a `setState` was done on this root in this particular `startTransition` call rather having already scheduled any Transition earlier. While async transitions are entangled, we don't know if there will be a startTransition+setState on a new root in the future. Therefore, we collect all transition types while this is happening and if a new root gets startTransition+setState they get added to that root. ```js startTransition(async () => { addTransitionType(...) await ...; setState(...); }); ```
1 parent deca965 commit 731ae3e

File tree

10 files changed

+227
-71
lines changed

10 files changed

+227
-71
lines changed

packages/react-reconciler/src/ReactFiberAsyncAction.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
enableComponentPerformanceTrack,
2626
enableProfilerTimer,
2727
} from 'shared/ReactFeatureFlags';
28+
import {clearEntangledAsyncTransitionTypes} from './ReactFiberTransitionTypes';
2829

2930
// If there are multiple, concurrent async actions, they are entangled. All
3031
// transition updates that occur while the async action is still in progress
@@ -84,6 +85,7 @@ function pingEngtangledActionScope() {
8485
clearAsyncTransitionTimer();
8586
}
8687
}
88+
clearEntangledAsyncTransitionTypes();
8789
if (currentEntangledListeners !== null) {
8890
// All the actions have finished. Close the entangled async action scope
8991
// and notify all the listeners.

packages/react-reconciler/src/ReactFiberHooks.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
enableLegacyCache,
4343
disableLegacyMode,
4444
enableNoCloningMemoCache,
45+
enableViewTransition,
4546
enableGestureTransition,
4647
} from 'shared/ReactFeatureFlags';
4748
import {
@@ -2159,6 +2160,17 @@ function runActionStateAction<S, P>(
21592160
// This is a fork of startTransition
21602161
const prevTransition = ReactSharedInternals.T;
21612162
const currentTransition: Transition = ({}: any);
2163+
if (enableViewTransition) {
2164+
currentTransition.types =
2165+
prevTransition !== null
2166+
? // If we're a nested transition, we should use the same set as the parent
2167+
// since we're conceptually always joined into the same entangled transition.
2168+
// In practice, this only matters if we add transition types in the inner
2169+
// without setting state. In that case, the inner transition can finish
2170+
// without waiting for the outer.
2171+
prevTransition.types
2172+
: null;
2173+
}
21622174
if (enableGestureTransition) {
21632175
currentTransition.gesture = null;
21642176
}
@@ -2180,6 +2192,24 @@ function runActionStateAction<S, P>(
21802192
} catch (error) {
21812193
onActionError(actionQueue, node, error);
21822194
} finally {
2195+
if (prevTransition !== null && currentTransition.types !== null) {
2196+
// If we created a new types set in the inner transition, we transfer it to the parent
2197+
// since they should share the same set. They're conceptually entangled.
2198+
if (__DEV__) {
2199+
if (
2200+
prevTransition.types !== null &&
2201+
prevTransition.types !== currentTransition.types
2202+
) {
2203+
// Just assert that assumption holds that we're not overriding anything.
2204+
console.error(
2205+
'We expected inner Transitions to have transferred the outer types set and ' +
2206+
'that you cannot add to the outer Transition while inside the inner.' +
2207+
'This is a bug in React.',
2208+
);
2209+
}
2210+
}
2211+
prevTransition.types = currentTransition.types;
2212+
}
21832213
ReactSharedInternals.T = prevTransition;
21842214

21852215
if (__DEV__) {
@@ -3052,6 +3082,17 @@ function startTransition<S>(
30523082

30533083
const prevTransition = ReactSharedInternals.T;
30543084
const currentTransition: Transition = ({}: any);
3085+
if (enableViewTransition) {
3086+
currentTransition.types =
3087+
prevTransition !== null
3088+
? // If we're a nested transition, we should use the same set as the parent
3089+
// since we're conceptually always joined into the same entangled transition.
3090+
// In practice, this only matters if we add transition types in the inner
3091+
// without setting state. In that case, the inner transition can finish
3092+
// without waiting for the outer.
3093+
prevTransition.types
3094+
: null;
3095+
}
30553096
if (enableGestureTransition) {
30563097
currentTransition.gesture = null;
30573098
}
@@ -3137,6 +3178,24 @@ function startTransition<S>(
31373178
} finally {
31383179
setCurrentUpdatePriority(previousPriority);
31393180

3181+
if (prevTransition !== null && currentTransition.types !== null) {
3182+
// If we created a new types set in the inner transition, we transfer it to the parent
3183+
// since they should share the same set. They're conceptually entangled.
3184+
if (__DEV__) {
3185+
if (
3186+
prevTransition.types !== null &&
3187+
prevTransition.types !== currentTransition.types
3188+
) {
3189+
// Just assert that assumption holds that we're not overriding anything.
3190+
console.error(
3191+
'We expected inner Transitions to have transferred the outer types set and ' +
3192+
'that you cannot add to the outer Transition while inside the inner.' +
3193+
'This is a bug in React.',
3194+
);
3195+
}
3196+
}
3197+
prevTransition.types = currentTransition.types;
3198+
}
31403199
ReactSharedInternals.T = prevTransition;
31413200

31423201
if (__DEV__) {

packages/react-reconciler/src/ReactFiberRoot.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import {
3333
enableUpdaterTracking,
3434
enableTransitionTracing,
3535
disableLegacyMode,
36+
enableViewTransition,
3637
enableGestureTransition,
3738
} from 'shared/ReactFeatureFlags';
3839
import {initializeUpdateQueue} from './ReactFiberClassUpdateQueue';
@@ -98,6 +99,10 @@ function FiberRootNode(
9899

99100
this.formState = formState;
100101

102+
if (enableViewTransition) {
103+
this.transitionTypes = null;
104+
}
105+
101106
if (enableGestureTransition) {
102107
this.pendingGestures = null;
103108
this.stoppingGestures = null;

packages/react-reconciler/src/ReactFiberTransition.js

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@ import type {
1212
GestureProvider,
1313
GestureOptions,
1414
} from 'shared/ReactTypes';
15-
import type {Lanes} from './ReactFiberLane';
15+
import {NoLane, type Lanes} from './ReactFiberLane';
1616
import type {StackCursor} from './ReactFiberStack';
1717
import type {Cache, SpawnedCachePool} from './ReactFiberCacheComponent';
1818
import type {Transition} from 'react/src/ReactStartTransition';
1919
import type {ScheduledGesture} from './ReactFiberGestureScheduler';
20-
import type {TransitionTypes} from 'react/src/ReactTransitionType';
2120

2221
import {
2322
enableTransitionTracing,
23+
enableViewTransition,
2424
enableGestureTransition,
2525
} from 'shared/ReactFeatureFlags';
2626
import {isPrimaryRenderer} from './ReactFiberConfig';
@@ -34,9 +34,17 @@ import {
3434
retainCache,
3535
CacheContext,
3636
} from './ReactFiberCacheComponent';
37+
import {
38+
queueTransitionTypes,
39+
entangleAsyncTransitionTypes,
40+
entangledTransitionTypes,
41+
} from './ReactFiberTransitionTypes';
3742

3843
import ReactSharedInternals from 'shared/ReactSharedInternals';
39-
import {entangleAsyncAction} from './ReactFiberAsyncAction';
44+
import {
45+
entangleAsyncAction,
46+
peekEntangledActionLane,
47+
} from './ReactFiberAsyncAction';
4048
import {startAsyncTransitionTimer} from './ReactProfilerTimer';
4149
import {firstScheduledRoot} from './ReactFiberRootScheduler';
4250
import {
@@ -87,6 +95,33 @@ ReactSharedInternals.S = function onStartTransitionFinishForReconciler(
8795
const thenable: Thenable<mixed> = (returnValue: any);
8896
entangleAsyncAction(transition, thenable);
8997
}
98+
if (enableViewTransition) {
99+
if (entangledTransitionTypes !== null) {
100+
// If we scheduled work on any new roots, we need to add any entangled async
101+
// transition types to those roots too.
102+
let root = firstScheduledRoot;
103+
while (root !== null) {
104+
queueTransitionTypes(root, entangledTransitionTypes);
105+
root = root.next;
106+
}
107+
}
108+
const transitionTypes = transition.types;
109+
if (transitionTypes !== null) {
110+
// Within this Transition we should've now scheduled any roots we have updates
111+
// to work on. If there are no updates on a root, then the Transition type won't
112+
// be applied to that root.
113+
let root = firstScheduledRoot;
114+
while (root !== null) {
115+
queueTransitionTypes(root, transitionTypes);
116+
root = root.next;
117+
}
118+
if (peekEntangledActionLane() !== NoLane) {
119+
// If we have entangled, async actions going on, the update associated with
120+
// these types might come later. We need to save them for later.
121+
entangleAsyncTransitionTypes(transitionTypes);
122+
}
123+
}
124+
}
90125
if (prevOnStartTransitionFinish !== null) {
91126
prevOnStartTransitionFinish(transition, returnValue);
92127
}
@@ -113,15 +148,13 @@ if (enableGestureTransition) {
113148
transition: Transition,
114149
provider: GestureProvider,
115150
options: ?GestureOptions,
116-
transitionTypes: null | TransitionTypes,
117151
): () => void {
118152
let cancel = null;
119153
if (prevOnStartGestureTransitionFinish !== null) {
120154
cancel = prevOnStartGestureTransitionFinish(
121155
transition,
122156
provider,
123157
options,
124-
transitionTypes,
125158
);
126159
}
127160
// For every root that has work scheduled, check if there's a ScheduledGesture
@@ -138,7 +171,7 @@ if (enableGestureTransition) {
138171
root,
139172
provider,
140173
options,
141-
transitionTypes,
174+
transition.types,
142175
);
143176
if (scheduledGesture !== null) {
144177
cancel = chainGestureCancellation(root, scheduledGesture, cancel);
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow
8+
*/
9+
10+
import type {FiberRoot} from './ReactInternalTypes';
11+
import type {TransitionTypes} from 'react/src/ReactTransitionType';
12+
13+
import {enableViewTransition} from 'shared/ReactFeatureFlags';
14+
import {includesTransitionLane} from './ReactFiberLane';
15+
16+
export function queueTransitionTypes(
17+
root: FiberRoot,
18+
transitionTypes: TransitionTypes,
19+
): void {
20+
if (enableViewTransition) {
21+
// TODO: We should really store transitionTypes per lane in a LaneMap on
22+
// the root. Then merge it when we commit. We currently assume that all
23+
// Transitions are entangled.
24+
if (includesTransitionLane(root.pendingLanes)) {
25+
let queued = root.transitionTypes;
26+
if (queued === null) {
27+
queued = root.transitionTypes = [];
28+
}
29+
for (let i = 0; i < transitionTypes.length; i++) {
30+
const transitionType = transitionTypes[i];
31+
if (queued.indexOf(transitionType) === -1) {
32+
queued.push(transitionType);
33+
}
34+
}
35+
}
36+
}
37+
}
38+
39+
// Store all types while we're entangled with an async Transition.
40+
export let entangledTransitionTypes: null | TransitionTypes = null;
41+
42+
export function entangleAsyncTransitionTypes(
43+
transitionTypes: TransitionTypes,
44+
): void {
45+
if (enableViewTransition) {
46+
let queued = entangledTransitionTypes;
47+
if (queued === null) {
48+
queued = entangledTransitionTypes = [];
49+
}
50+
for (let i = 0; i < transitionTypes.length; i++) {
51+
const transitionType = transitionTypes[i];
52+
if (queued.indexOf(transitionType) === -1) {
53+
queued.push(transitionType);
54+
}
55+
}
56+
}
57+
}
58+
59+
export function clearEntangledAsyncTransitionTypes() {
60+
// Called when all Async Actions are done.
61+
entangledTransitionTypes = null;
62+
}
63+
64+
export function claimQueuedTransitionTypes(
65+
root: FiberRoot,
66+
): null | TransitionTypes {
67+
const claimed = root.transitionTypes;
68+
root.transitionTypes = null;
69+
return claimed;
70+
}

packages/react-reconciler/src/ReactFiberWorkLoop.js

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,7 @@ import {
358358
deleteScheduledGesture,
359359
stopCompletedGestures,
360360
} from './ReactFiberGestureScheduler';
361+
import {claimQueuedTransitionTypes} from './ReactFiberTransitionTypes';
361362

362363
const PossiblyWeakMap = typeof WeakMap === 'function' ? WeakMap : Map;
363364

@@ -3404,11 +3405,7 @@ function commitRoot(
34043405
pendingViewTransitionEvents = null;
34053406
if (includesOnlyViewTransitionEligibleLanes(lanes)) {
34063407
// Claim any pending Transition Types for this commit.
3407-
// This means that multiple roots committing independent View Transitions
3408-
// 1) end up staggered because we can only have one at a time.
3409-
// 2) only the first one gets all the Transition Types.
3410-
pendingTransitionTypes = ReactSharedInternals.V;
3411-
ReactSharedInternals.V = null;
3408+
pendingTransitionTypes = claimQueuedTransitionTypes(root);
34123409
passiveSubtreeMask = PassiveTransitionMask;
34133410
} else {
34143411
pendingTransitionTypes = null;

packages/react-reconciler/src/ReactInternalTypes.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import type {
1818
ReactComponentInfo,
1919
ReactDebugInfo,
2020
} from 'shared/ReactTypes';
21+
import type {TransitionTypes} from 'react/src/ReactTransitionType';
2122
import type {WorkTag} from './ReactWorkTags';
2223
import type {TypeOfMode} from './ReactTypeOfMode';
2324
import type {Flags} from './ReactFiberFlags';
@@ -280,6 +281,8 @@ type BaseFiberRootProperties = {
280281

281282
formState: ReactFormState<any, any> | null,
282283

284+
// enableViewTransition only
285+
transitionTypes: null | TransitionTypes, // TODO: Make this a LaneMap.
283286
// enableGestureTransition only
284287
pendingGestures: null | ScheduledGesture,
285288
stoppingGestures: null | ScheduledGesture,

packages/react/src/ReactSharedInternalsClient.js

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,15 @@
1010
import type {Dispatcher} from 'react-reconciler/src/ReactInternalTypes';
1111
import type {AsyncDispatcher} from 'react-reconciler/src/ReactInternalTypes';
1212
import type {Transition} from './ReactStartTransition';
13-
import type {TransitionTypes} from './ReactTransitionType';
1413
import type {GestureProvider, GestureOptions} from 'shared/ReactTypes';
1514

16-
import {
17-
enableViewTransition,
18-
enableGestureTransition,
19-
} from 'shared/ReactFeatureFlags';
15+
import {enableGestureTransition} from 'shared/ReactFeatureFlags';
2016

2117
type onStartTransitionFinish = (Transition, mixed) => void;
2218
type onStartGestureTransitionFinish = (
2319
Transition,
2420
GestureProvider,
2521
?GestureOptions,
26-
transitionTypes: null | TransitionTypes,
2722
) => () => void;
2823

2924
export type SharedStateClient = {
@@ -32,7 +27,6 @@ export type SharedStateClient = {
3227
T: null | Transition, // ReactCurrentBatchConfig for Transitions
3328
S: null | onStartTransitionFinish,
3429
G: null | onStartGestureTransitionFinish,
35-
V: null | TransitionTypes, // Pending Transition Types for the Next Transition
3630

3731
// DEV-only
3832

@@ -72,9 +66,6 @@ const ReactSharedInternals: SharedStateClient = ({
7266
if (enableGestureTransition) {
7367
ReactSharedInternals.G = null;
7468
}
75-
if (enableViewTransition) {
76-
ReactSharedInternals.V = null;
77-
}
7869

7970
if (__DEV__) {
8071
ReactSharedInternals.actQueue = null;

0 commit comments

Comments
 (0)