[Fizz] Pass batch as argument to revealCompletedBoundaries (#33330)
Follow up to #33293. This solves a race condition when boundaries are added to the batch after the `startViewTransition` call. This doesn't matter yet but it will once we start assigning names before the `startViewTransition` call. A possible alternative solution might be to ensure the names are added synchronously in the event that adds to the batch. It's possible to keep adding to a batch until the snapshot has happened.
Sebastian Markbåge committed
May 22, 2025 at 10:25 UTC
91ac1fea1aacf80c9eb2815956e230921b55808c
2 files changed
+18
-10
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js
+2
-2
@@ -6,9 +6,9 @@ export const markShellTime =
6
export const clientRenderBoundary =
7
'$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())};';
8
export const completeBoundary =
9
- '$RB=[];$RV=function(){$RT=performance.now();var d=$RB;$RB=[];for(var a=0;a<d.length;a+=2){var b=d[a],h=d[a+1],e=b.parentNode;if(e){var f=b.previousSibling,g=0;do{if(b&&8===b.nodeType){var c=b.data;if("/$"===c||"/&"===c)if(0===g)break;else g--;else"$"!==c&&"$?"!==c&&"$~"!==c&&"$!"!==c&&"&"!==c||g++}c=b.nextSibling;e.removeChild(b);b=c}while(b);for(;h.firstChild;)e.insertBefore(h.firstChild,b);f.data="$";f._reactRetry&&f._reactRetry()}}};$RC=function(d,a){if(a=document.getElementById(a))if(a.parentNode.removeChild(a),d=document.getElementById(d))d.previousSibling.data="$~",$RB.push(d,a),2===$RB.length&&setTimeout($RV,("number"!==typeof $RT?0:$RT)+300-performance.now())};';
9
+ '$RB=[];$RV=function(c){$RT=performance.now();for(var a=0;a<c.length;a+=2){var b=c[a],h=c[a+1],e=b.parentNode;if(e){var f=b.previousSibling,g=0;do{if(b&&8===b.nodeType){var d=b.data;if("/$"===d||"/&"===d)if(0===g)break;else g--;else"$"!==d&&"$?"!==d&&"$~"!==d&&"$!"!==d&&"&"!==d||g++}d=b.nextSibling;e.removeChild(b);b=d}while(b);for(;h.firstChild;)e.insertBefore(h.firstChild,b);f.data="$";f._reactRetry&&f._reactRetry()}}c.length=0};$RC=function(c,a){if(a=document.getElementById(a))if(a.parentNode.removeChild(a),c=document.getElementById(c))c.previousSibling.data="$~",$RB.push(c,a),2===$RB.length&&setTimeout($RV.bind(null,$RB),("number"!==typeof $RT?0:$RT)+300-performance.now())};';
10
export const completeBoundaryUpgradeToViewTransitions =
11
- '$RV=function(a){try{var b=document.__reactViewTransition;if(b){b.finished.then($RV,$RV);return}if(window._useVT){var c=document.__reactViewTransition=document.startViewTransition({update:a,types:[]});c.finished.finally(function(){document.__reactViewTransition===c&&(document.__reactViewTransition=null)});return}}catch(d){}a()}.bind(null,$RV);';
11
+ '$RV=function(c,a){try{var d=document.__reactViewTransition;if(d){d.finished.finally($RV.bind(null,a));return}if(window._useVT){var b=document.__reactViewTransition=document.startViewTransition({update:c.bind(null,a),types:[]});b.ready.finally(function(){});b.finished.finally(function(){document.__reactViewTransition===b&&(document.__reactViewTransition=null)});$RB=[];return}}catch(e){}c(a)}.bind(null,$RV);';
12
export const completeBoundaryWithStyles =
13
'$RM=new Map;$RR=function(n,w,p){function u(q){this._p=null;q()}for(var r=new Map,t=document,h,b,e=t.querySelectorAll("link[data-precedence],style[data-precedence]"),v=[],k=0;b=e[k++];)"not all"===b.getAttribute("media")?v.push(b):("LINK"===b.tagName&&$RM.set(b.getAttribute("href"),b),r.set(b.dataset.precedence,h=b));e=0;b=[];var l,a;for(k=!0;;){if(k){var f=p[e++];if(!f){k=!1;e=0;continue}var c=!1,m=0;var d=f[m++];if(a=$RM.get(d)){var g=a._p;c=!0}else{a=t.createElement("link");a.href=d;a.rel=\n"stylesheet";for(a.dataset.precedence=l=f[m++];g=f[m++];)a.setAttribute(g,f[m++]);g=a._p=new Promise(function(q,x){a.onload=u.bind(a,q);a.onerror=u.bind(a,x)});$RM.set(d,a)}d=a.getAttribute("media");!g||d&&!matchMedia(d).matches||b.push(g);if(c)continue}else{a=v[e++];if(!a)break;l=a.getAttribute("data-precedence");a.removeAttribute("media")}c=r.get(l)||h;c===h&&(h=a);r.set(l,a);c?c.parentNode.insertBefore(a,c.nextSibling):(c=t.head,c.insertBefore(a,c.firstChild))}if(p=document.getElementById(n))p.previousSibling.data=\n"$~";Promise.all(b).then($RC.bind(null,n,w),$RX.bind(null,n,"CSS failed to load"))};';
14
export const completeSegment =
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetShared.js
+16
-8
@@ -18,10 +18,8 @@ const SUSPENSE_FALLBACK_START_DATA = '$!';
18
// working. Closure converts it to a dot access anyway, though, so it's not an
19
// urgent issue.
20
21
-export function revealCompletedBoundaries() {
21
+export function revealCompletedBoundaries(batch) {
22
window['$RT'] = performance.now();
23
- const batch = window['$RB'];
24
- window['$RB'] = [];
23
for (let i = 0; i < batch.length; i += 2) {
24
const suspenseIdNode = batch[i];
25
const contentNode = batch[i + 1];
@@ -79,14 +77,18 @@ export function revealCompletedBoundaries() {
77
suspenseNode['_reactRetry']();
78
}
79
}
80
+ batch.length = 0;
81
}
82
84
-export function revealCompletedBoundariesWithViewTransitions(revealBoundaries) {
83
+export function revealCompletedBoundariesWithViewTransitions(
84
+ revealBoundaries,
85
+ batch,
86
+) {
87
try {
88
const existingTransition = document['__reactViewTransition'];
89
if (existingTransition) {
90
// Retry after the previous ViewTransition finishes.
89
- existingTransition.finished.then(window['$RV'], window['$RV']);
91
+ existingTransition.finished.finally(window['$RV'].bind(null, batch));
92
return;
93
}
94
const shouldStartViewTransition = window['_useVT']; // TODO: Detect.
@@ -94,14 +96,20 @@ export function revealCompletedBoundariesWithViewTransitions(revealBoundaries) {
96
const transition = (document['__reactViewTransition'] = document[
97
'startViewTransition'
98
]({
97
- update: revealBoundaries,
99
+ update: revealBoundaries.bind(null, batch),
100
types: [], // TODO: Add a hard coded type for Suspense reveals.
101
}));
102
+ transition.ready.finally(() => {
103
+ // TODO
104
+ });
105
transition.finished.finally(() => {
106
if (document['__reactViewTransition'] === transition) {
107
document['__reactViewTransition'] = null;
108
}
109
});
110
+ // Queue any future completions into its own batch since they won't have been
111
+ // snapshotted by this one.
112
+ window['$RB'] = [];
113
return;
114
}
115
// Fall through to reveal.
@@ -109,7 +117,7 @@ export function revealCompletedBoundariesWithViewTransitions(revealBoundaries) {
117
// Fall through to reveal.
118
}
119
// ViewTransitions v2 not supported or no ViewTransitions found. Reveal immediately.
112
- revealBoundaries();
120
+ revealBoundaries(batch);
121
}
122
123
export function clientRenderBoundary(
@@ -182,7 +190,7 @@ export function completeBoundary(suspenseBoundaryID, contentID) {
190
// We always schedule the flush in a timer even if it's very low or negative to allow
191
// for multiple completeBoundary calls that are already queued to have a chance to
192
// make the batch.
185
- setTimeout(window['$RV'], msUntilTimeout);
193
+ setTimeout(window['$RV'].bind(null, window['$RB']), msUntilTimeout);
194
}
195
}
196