Repository: facebook/react
Pull request: Materialize the tree ID when ViewTransition name=auto consumes one

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.


Changed files:
filename: fixtures/view-transition/src/components/Page.js
patch: @@ -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>

filename: packages/react-reconciler/src/ReactFiberBeginWork.js
patch: @@ -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.

filename: packages/react-server/src/ReactFizzServer.js
patch: @@ -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