Remove useId semantics from View Transition name generation (#33094)
Originally I thought it was important that SSR used the same View Transition name as the client so that the Fizz runtime could emit those names and then the client could pick up and take over. However, I no longer believe that approach is feasible. Instead, the names can be generated only during that particular animation. Therefore we can simplify the auto name assignment to not have to consider the hydration.
Sebastian Markbåge committed
May 6, 2025 at 10:33 UTC
845d93742fb090e7a35abea409a55e2a14613255
4 files changed
+15
-64
packages/react-reconciler/src/ReactFiberBeginWork.js
-13
@@ -36,8 +36,6 @@ import type {
36
OffscreenQueue,
37
OffscreenInstance,
38
} from './ReactFiberOffscreenComponent';
39
-import type {ViewTransitionState} from './ReactFiberViewTransitionComponent';
40
-import {assignViewTransitionAutoName} from './ReactFiberViewTransitionComponent';
39
import type {
40
Cache,
41
CacheComponentState,
@@ -3538,7 +3536,6 @@ function updateViewTransition(
3536
renderLanes: Lanes,
3537
) {
3538
const pendingProps: ViewTransitionProps = workInProgress.pendingProps;
3541
- const instance: ViewTransitionState = workInProgress.stateNode;
3539
if (pendingProps.name != null && pendingProps.name !== 'auto') {
3540
// Explicitly named boundary. We track it so that we can pair it up with another explicit
3541
// boundary if we get deleted.
@@ -3546,16 +3543,6 @@ function updateViewTransition(
3543
current === null
3544
? ViewTransitionNamedMount | ViewTransitionNamedStatic
3545
: ViewTransitionNamedStatic;
3549
- } else {
3550
- // Assign an auto generated name using the useId algorthim if an explicit one is not provided.
3551
- // We don't need the name yet but we do it here to allow hydration state to be used.
3552
- // We might end up needing these to line up if we want to Transition from dehydrated fallback
3553
- // to client rendered content. If we don't end up using that we could just assign an incremeting
3554
- // counter in the commit phase instead.
3555
- assignViewTransitionAutoName(pendingProps, instance);
3556
- if (getIsHydrating()) {
3557
- pushMaterializedTreeId(workInProgress);
3558
- }
3546
}
3547
if (__DEV__) {
3548
// $FlowFixMe[prop-missing]
packages/react-reconciler/src/ReactFiberViewTransitionComponent.js
+10
-34
@@ -12,14 +12,10 @@ import type {FiberRoot} from './ReactInternalTypes';
12
import type {ViewTransitionInstance, Instance} from './ReactFiberConfig';
13
14
import {
15
- getWorkInProgressRoot,
15
+ getCommittingRoot,
16
getPendingTransitionTypes,
17
} from './ReactFiberWorkLoop';
18
19
-import {getIsHydrating} from './ReactFiberHydrationContext';
20
-
21
-import {getTreeId} from './ReactFiberTreeContext';
22
-
19
export type ViewTransitionState = {
20
autoName: null | string, // the view-transition-name to use when an explicit one is not specified
21
paired: null | ViewTransitionState, // a temporary state during the commit phase if we have paired this with another instance
@@ -29,47 +25,27 @@ export type ViewTransitionState = {
25
26
let globalClientIdCounter: number = 0;
27
32
-export function assignViewTransitionAutoName(
28
+export function getViewTransitionName(
29
props: ViewTransitionProps,
30
instance: ViewTransitionState,
31
): string {
32
+ if (props.name != null && props.name !== 'auto') {
33
+ return props.name;
34
+ }
35
if (instance.autoName !== null) {
36
return instance.autoName;
37
}
38
40
- const root = ((getWorkInProgressRoot(): any): FiberRoot);
39
+ // We assume we always call this in the commit phase.
40
+ const root = ((getCommittingRoot(): any): FiberRoot);
41
const identifierPrefix = root.identifierPrefix;
42
-
43
- let name;
44
- if (getIsHydrating()) {
45
- const treeId = getTreeId();
46
- // Use a captial R prefix for server-generated ids.
47
- name = '\u00AB' + identifierPrefix + 'T' + treeId + '\u00BB';
48
- } else {
49
- // Use a lowercase r prefix for client-generated ids.
50
- const globalClientId = globalClientIdCounter++;
51
- name =
52
- '\u00AB' +
53
- identifierPrefix +
54
- 't' +
55
- globalClientId.toString(32) +
56
- '\u00BB';
57
- }
42
+ const globalClientId = globalClientIdCounter++;
43
+ const name =
44
+ '\u00AB' + identifierPrefix + 't' + globalClientId.toString(32) + '\u00BB';
45
instance.autoName = name;
46
return name;
47
}
48
62
-export function getViewTransitionName(
63
- props: ViewTransitionProps,
64
- instance: ViewTransitionState,
65
-): string {
66
- if (props.name != null && props.name !== 'auto') {
67
- return props.name;
68
- }
69
- // We should have assigned a name by now.
70
- return (instance.autoName: any);
71
-}
72
-
49
function getClassNameByType(classByType: ?ViewTransitionClass): ?string {
50
if (classByType == null || typeof classByType === 'string') {
51
return classByType;
packages/react-reconciler/src/ReactFiberWorkLoop.js
+4
@@ -700,6 +700,10 @@ export function getWorkInProgressRoot(): FiberRoot | null {
700
return workInProgressRoot;
701
}
702
703
+export function getCommittingRoot(): FiberRoot | null {
704
+ return pendingEffectsRoot;
705
+}
706
+
707
export function getWorkInProgressRootRenderLanes(): Lanes {
708
return workInProgressRootRenderLanes;
709
}
packages/react-server/src/ReactFizzServer.js
+1
-17
@@ -2274,23 +2274,7 @@ function renderViewTransition(
2274
) {
2275
const prevKeyPath = task.keyPath;
2276
task.keyPath = keyPath;
2277
- if (props.name != null && props.name !== 'auto') {
2278
- renderNodeDestructive(request, task, props.children, -1);
2279
- } else {
2280
- // This will be auto-assigned a name which claims a "useId" slot.
2281
- // This component materialized an id. We treat this as its own level, with
2282
- // a single "child" slot.
2283
- const prevTreeContext = task.treeContext;
2284
- const totalChildren = 1;
2285
- const index = 0;
2286
- // Modify the id context. Because we'll need to reset this if something
2287
- // suspends or errors, we'll use the non-destructive render path.
2288
- task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index);
2289
- renderNode(request, task, props.children, -1);
2290
- // Like the other contexts, this does not need to be in a finally block
2291
- // because renderNode takes care of unwinding the stack.
2292
- task.treeContext = prevTreeContext;
2293
- }
2277
+ renderNodeDestructive(request, task, props.children, -1);
2278
task.keyPath = prevKeyPath;
2279
}
2280