Materialize the tree ID when ViewTransition name=auto consumes one (#32651)

ViewTransition uses the `useId` algorithm to auto-assign names. This
ensures that we could animate between SSR content and client content by
ensuring that the names line up.

However, I missed that we need to bump the id (materialize it) when we
do that. This is what function components do if they use one or more
`useId()`. This caused duplicate names when two ViewTransitions were
nested without any siblings since they would share name.
This commit is contained in:
Sebastian Markbåge 2025-03-17 16:17:01 -04:00 committed by GitHub
parent ca02c4bb40
commit 9fde224a53
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 47 additions and 7 deletions

View File

@ -4,6 +4,7 @@ import React, {
unstable_useSwipeTransition as useSwipeTransition,
useEffect,
useState,
useId,
} from 'react';
import SwipeRecognizer from './SwipeRecognizer';
@ -39,6 +40,11 @@ function Component() {
);
}
function Id() {
// This is just testing that Id inside a ViewTransition can hydrate correctly.
return <span id={useId()} />;
}
export default function Page({url, navigate}) {
const [renderedUrl, startGesture] = useSwipeTransition('/?a', url, '/?b');
const show = renderedUrl === '/?b';
@ -77,8 +83,12 @@ export default function Page({url, navigate}) {
</button>
<ViewTransition className="none">
<div>
<ViewTransition className={transitions['slide-on-nav']}>
<h1>{!show ? 'A' : 'B'}</h1>
<ViewTransition>
<div>
<ViewTransition className={transitions['slide-on-nav']}>
<h1>{!show ? 'A' : 'B'}</h1>
</ViewTransition>
</div>
</ViewTransition>
<ViewTransition
className={{
@ -102,7 +112,9 @@ export default function Page({url, navigate}) {
{show ? <div>hello{exclamation}</div> : <section>Loading</section>}
</ViewTransition>
<p>scroll me</p>
<p></p>
<p>
<Id />
</p>
<p></p>
<p></p>
<p></p>

View File

@ -3298,6 +3298,9 @@ function updateViewTransition(
// to client rendered content. If we don't end up using that we could just assign an incremeting
// counter in the commit phase instead.
assignViewTransitionAutoName(pendingProps, instance);
if (getIsHydrating()) {
pushMaterializedTreeId(workInProgress);
}
}
if (current !== null && current.memoizedProps.name !== pendingProps.name) {
// If the name changes, we schedule a ref effect to create a new ref instance.

View File

@ -2211,6 +2211,34 @@ function renderOffscreen(
}
}
function renderViewTransition(
request: Request,
task: Task,
keyPath: KeyNode,
props: Object,
) {
const prevKeyPath = task.keyPath;
task.keyPath = keyPath;
if (props.name != null && props.name !== 'auto') {
renderNodeDestructive(request, task, props.children, -1);
} else {
// This will be auto-assigned a name which claims a "useId" slot.
// This component materialized an id. We treat this as its own level, with
// a single "child" slot.
const prevTreeContext = task.treeContext;
const totalChildren = 1;
const index = 0;
// Modify the id context. Because we'll need to reset this if something
// suspends or errors, we'll use the non-destructive render path.
task.treeContext = pushTreeContext(prevTreeContext, totalChildren, index);
renderNode(request, task, props.children, -1);
// Like the other contexts, this does not need to be in a finally block
// because renderNode takes care of unwinding the stack.
task.treeContext = prevTreeContext;
}
task.keyPath = prevKeyPath;
}
function renderElement(
request: Request,
task: Task,
@ -2267,10 +2295,7 @@ function renderElement(
}
case REACT_VIEW_TRANSITION_TYPE: {
if (enableViewTransition) {
const prevKeyPath = task.keyPath;
task.keyPath = keyPath;
renderNodeDestructive(request, task, props.children, -1);
task.keyPath = prevKeyPath;
renderViewTransition(request, task, keyPath, props);
return;
}
// Fallthrough