Repository: facebook/react
Pull request: [Fizz] Delay detachment of completed boundaries until reveal

## Summary

Fixes a deopt to client-render if we throttled the reveal of a boundary that contained other boundaries that completed before the throttled reveal.
This got reported as "The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering." which was a red herring in this case.

## How did you test this change?

- Added repro to VT fixture. No longer errors with "The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering."


Changed files:
filename: fixtures/view-transition/src/components/App.js
patch: @@ -4,6 +4,7 @@ import React, {
   useEffect,
   useState,
   unstable_addTransitionType as addTransitionType,
+  use,
 } from 'react';
 
 import Chrome from './Chrome';

filename: fixtures/view-transition/src/components/NestedReveal.js
patch: @@ -0,0 +1,36 @@
+import React, {Suspense, use} from 'react';
+
+async function sleep(ms) {
+  return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+function Use({useable}) {
+  use(useable);
+  return null;
+}
+
+let delay1;
+let delay2;
+
+export default function NestedReveal({}) {
+  if (!delay1) {
+    delay1 = sleep(100);
+    // Needs to happen before the throttled reveal of delay 1
+    delay2 = sleep(200);
+  }
+
+  return (
+    <div className="swipe-recognizer">
+      Shell
+      <Suspense fallback="Loading level 1">
+        <div>Level 1</div>
+        <Use useable={delay1} />
+
+        <Suspense fallback="Loading level 2">
+          <div>Level 2</div>
+          <Use useable={delay2} />
+        </Suspense>
+      </Suspense>
+    </div>
+  );
+}

filename: fixtures/view-transition/src/components/Page.js
patch: @@ -18,6 +18,7 @@ import SwipeRecognizer from './SwipeRecognizer';
 import './Page.css';
 
 import transitions from './Transitions.module.css';
+import NestedReveal from './NestedReveal';
 
 async function sleep(ms) {
   return new Promise(resolve => setTimeout(resolve, ms));
@@ -241,6 +242,7 @@ export default function Page({url, navigate}) {
           </div>
         </ViewTransition>
       </SwipeRecognizer>
+      <NestedReveal />
     </div>
   );
 }

filename: packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js
patch: @@ -6,7 +6,7 @@ export const markShellTime =
 export const clientRenderBoundary =
   '$RX=function(b,c,d,e,f){var a=document.getElementById(b);a&&(b=a.previousSibling,b.data="$!",a=a.dataset,c&&(a.dgst=c),d&&(a.msg=d),e&&(a.stck=e),f&&(a.cstck=f),b._reactRetry&&b._reactRetry())};';
 export const completeBoundary =
-  '$RB=[];$RV=function(b){$RT=performance.now();for(var a=0;a<b.length;a+=2){var c=b[a],h=b[a+1],e=c.parentNode;if(e){var f=c.previousSibling,g=0;do{if(c&&8===c.nodeType){var d=c.data;if("/$"===d||"/&"===d)if(0===g)break;else g--;else"$"!==d&&"$?"!==d&&"$~"!==d&&"$!"!==d&&"&"!==d||g++}d=c.nextSibling;e.removeChild(c);c=d}while(c);for(;h.firstChild;)e.insertBefore(h.firstChild,c);f.data="$";f._reactRetry&&f._reactRetry()}}b.length=0};$RC=function(b,a){if(a=document.getElementById(a))if(a.parentNode.removeChild(a),b=document.getElementById(b))b.previousSibling.data="$~",$RB.push(b,a),2===$RB.length&&(b="number"!==typeof $RT?0:$RT,a=performance.now(),setTimeout($RV.bind(null,$RB),2300>a&&2E3<a?2300-a:b+300-a))};';
+  '$RB=[];$RV=function(b){$RT=performance.now();for(var a=0;a<b.length;a+=2){var c=b[a],e=b[a+1];e.parentNode.removeChild(e);var f=c.parentNode;if(f){var g=c.previousSibling,h=0;do{if(c&&8===c.nodeType){var d=c.data;if("/$"===d||"/&"===d)if(0===h)break;else h--;else"$"!==d&&"$?"!==d&&"$~"!==d&&"$!"!==d&&"&"!==d||h++}d=c.nextSibling;f.removeChild(c);c=d}while(c);for(;e.firstChild;)f.insertBefore(e.firstChild,c);g.data="$";g._reactRetry&&g._reactRetry()}}b.length=0};$RC=function(b,a){if(a=document.getElementById(a))(b=document.getElementById(b))?(b.previousSibling.data="$~",$RB.push(b,a),2===$RB.length&&(b="number"!==typeof $RT?0:$RT,a=performance.now(),setTimeout($RV.bind(null,$RB),2300>a&&2E3<a?2300-a:b+300-a))):a.parentNode.removeChild(a)};';
 export const completeBoundaryUpgradeToViewTransitions =
   '$RV=function(A,g){function k(a,b){var e=a.getAttribute(b);e&&(b=a.style,l.push(a,b.viewTransitionName,b.viewTransitionClass),"auto"!==e&&(b.viewTransitionClass=e),(a=a.getAttribute("vt-name"))||(a="_T_"+K++ +"_"),b.viewTransitionName=a,B=!0)}var B=!1,K=0,l=[];try{var f=document.__reactViewTransition;if(f){f.finished.finally($RV.bind(null,g));return}var m=new Map;for(f=1;f<g.length;f+=2)for(var h=g[f].querySelectorAll("[vt-share]"),d=0;d<h.length;d++){var c=h[d];m.set(c.getAttribute("vt-name"),c)}var u=[];for(h=0;h<g.length;h+=2){var C=g[h],x=C.parentNode;if(x){var v=x.getBoundingClientRect();if(v.left||v.top||v.width||v.height){c=C;for(f=0;c;){if(8===c.nodeType){var r=c.data;if("/$"===r)if(0===f)break;else f--;else"$"!==r&&"$?"!==r&&"$~"!==r&&"$!"!==r||f++}else if(1===c.nodeType){d=c;var D=d.getAttribute("vt-name"),y=m.get(D);k(d,y?"vt-share":"vt-exit");y&&(k(y,"vt-share"),m.set(D,null));var E=d.querySelectorAll("[vt-share]");for(d=0;d<E.length;d++){var F=E[d],G=F.getAttribute("vt-name"),\nH=m.get(G);H&&(k(F,"vt-share"),k(H,"vt-share"),m.set(G,null))}}c=c.nextSibling}for(var I=g[h+1],t=I.firstElementChild;t;)null!==m.get(t.getAttribute("vt-name"))&&k(t,"vt-enter"),t=t.nextElementSibling;c=x;do for(var n=c.firstElementChild;n;){var J=n.getAttribute("vt-update");J&&"none"!==J&&!l.includes(n)&&k(n,"vt-update");n=n.nextElementSibling}while((c=c.parentNode)&&1===c.nodeType&&"none"!==c.getAttribute("vt-update"));u.push.apply(u,I.querySelectorAll(\'img[src]:not([loading="lazy"])\'))}}}if(B){var z=\ndocument.__reactViewTransition=document.startViewTransition({update:function(){A(g);for(var a=[document.documentElement.clientHeight,document.fonts.ready],b={},e=0;e<u.length;b={g:b.g},e++)if(b.g=u[e],!b.g.complete){var p=b.g.getBoundingClientRect();0<p.bottom&&0<p.right&&p.top<window.innerHeight&&p.left<window.innerWidth&&(p=new Promise(function(w){return function(q){w.g.addEventListener("load",q);w.g.addEventListener("error",q)}}(b)),a.push(p))}return Promise.race([Promise.all(a),new Promise(function(w){var q=\nperformance.now();setTimeout(w,2300>q&&2E3<q?2300-q:500)})])},types:[]});z.ready.finally(function(){for(var a=l.length-3;0<=a;a-=3){var b=l[a],e=b.style;e.viewTransitionName=l[a+1];e.viewTransitionClass=l[a+1];""===b.getAttribute("style")&&b.removeAttribute("style")}});z.finished.finally(function(){document.__reactViewTransition===z&&(document.__reactViewTransition=null)});$RB=[];return}}catch(a){}A(g)}.bind(null,$RV);';
 export const completeBoundaryWithStyles =

filename: packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetShared.js
patch: @@ -34,6 +34,10 @@ export function revealCompletedBoundaries(batch) {
   for (let i = 0; i < batch.length; i += 2) {
     const suspenseIdNode = batch[i];
     const contentNode = batch[i + 1];
+    // We can detach the content now.
+    // Completions of boundaries within this contentNode will now find the boundary
+    // in its designated place.
+    contentNode.parentNode.removeChild(contentNode);
 
     // Clear all the existing children. This is complicated because
     // there can be embedded Suspense boundaries in the fallback.
@@ -385,13 +389,16 @@ export function completeBoundary(suspenseBoundaryID, contentID) {
     // the segment. Regardless we can ignore this case.
     return;
   }
-  // We'll detach the content node so that regardless of what happens next we don't leave in the tree.
-  // This might also help by not causing recalcing each time we move a child from here to the target.
-  contentNodeOuter.parentNode.removeChild(contentNodeOuter);
 
   // Find the fallback's first element.
   const suspenseIdNodeOuter = document.getElementById(suspenseBoundaryID);
   if (!suspenseIdNodeOuter) {
+    // We'll never reveal this boundary so we can remove its content immediately.
+    // Otherwise we'll leave it in until we reveal it.
+    // This is important in case this specific boundary contains other boundaries
+    // that may get completed before we reveal this one.
+    contentNodeOuter.parentNode.removeChild(contentNodeOuter);
+
     // The user must have already navigated away from this tree.
     // E.g. because the parent was hydrated. That's fine there's nothing to do
     // but we have to make sure that we already deleted the container node.