[Fizz] Wrap revealCompletedBoundaries in a ViewTransitions aware version (#33293)
When needed. For the external runtime we always include this wrapper. For others, we only include it if we have an ViewTransitions affecting. If we discover the ViewTransitions late, then we can upgrade an already emitted instruction. This doesn't yet do anything useful with it, that's coming in a follow up. This is just the mechanism for how it gets installed.
Sebastian Markbåge committed
May 17, 2025 at 18:18 UTC
6060367ef8a7a5bac12e0f830367bb13626db83a
12 files changed
+259
-109
fixtures/view-transition/server/render.js
+2
@@ -23,6 +23,8 @@ export default function render(url, res) {
23
const {pipe, abort} = renderToPipeableStream(
24
<App assets={assets} initialURL={url} />,
25
{
26
+ // TODO: Temporary hack. Detect from attributes instead.
27
+ bootstrapScriptContent: 'window._useVT = true;',
28
bootstrapScripts: [assets['main.js']],
29
onShellReady() {
30
// If something errored before we started streaming, we set the error code appropriately.
fixtures/view-transition/src/components/Page.js
+25
-13
@@ -8,6 +8,7 @@ import React, {
8
useId,
9
useOptimistic,
10
startTransition,
11
+ Suspense,
12
} from 'react';
13
14
import {createPortal} from 'react-dom';
@@ -60,6 +61,12 @@ function Id() {
61
return <span id={useId()} />;
62
}
63
64
+let wait;
65
+function Suspend() {
66
+ if (!wait) wait = sleep(500);
67
+ return React.use(wait);
68
+}
69
+
70
export default function Page({url, navigate}) {
71
const [renderedUrl, optimisticNavigate] = useOptimistic(
72
url,
@@ -93,7 +100,7 @@ export default function Page({url, navigate}) {
100
// a flushSync will.
101
// Promise.resolve().then(() => {
102
// flushSync(() => {
96
- setCounter(c => c + 10);
103
+ // setCounter(c => c + 10);
104
// });
105
// });
106
}, [show]);
@@ -193,18 +200,23 @@ export default function Page({url, navigate}) {
200
<div>!!</div>
201
</ViewTransition>
202
</Activity>
196
- <p>these</p>
197
- <p>rows</p>
198
- <p>exist</p>
199
- <p>to</p>
200
- <p>test</p>
201
- <p>scrolling</p>
202
- <p>content</p>
203
- <p>out</p>
204
- <p>of</p>
205
- {portal}
206
- <p>the</p>
207
- <p>viewport</p>
203
+ <Suspense fallback="Loading">
204
+ <ViewTransition>
205
+ <p>these</p>
206
+ <p>rows</p>
207
+ <p>exist</p>
208
+ <p>to</p>
209
+ <p>test</p>
210
+ <p>scrolling</p>
211
+ <p>content</p>
212
+ <p>out</p>
213
+ <p>of</p>
214
+ {portal}
215
+ <p>the</p>
216
+ <p>viewport</p>
217
+ <Suspend />
218
+ </ViewTransition>
219
+ </Suspense>
220
{show ? <Component /> : null}
221
</div>
222
</ViewTransition>
packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
+77
-23
@@ -80,6 +80,7 @@ import isArray from 'shared/isArray';
80
import {
81
clientRenderBoundary as clientRenderFunction,
82
completeBoundary as completeBoundaryFunction,
83
+ completeBoundaryUpgradeToViewTransitions as upgradeToViewTransitionsInstruction,
84
completeBoundaryWithStyles as styleInsertionFunction,
85
completeSegment as completeSegmentFunction,
86
formReplaying as formReplayingRuntime,
@@ -123,14 +124,16 @@ const ScriptStreamingFormat: StreamingFormat = 0;
124
const DataStreamingFormat: StreamingFormat = 1;
125
126
export type InstructionState = number;
126
-const NothingSent /* */ = 0b0000000;
127
-const SentCompleteSegmentFunction /* */ = 0b0000001;
128
-const SentCompleteBoundaryFunction /* */ = 0b0000010;
129
-const SentClientRenderFunction /* */ = 0b0000100;
130
-const SentStyleInsertionFunction /* */ = 0b0001000;
131
-const SentFormReplayingRuntime /* */ = 0b0010000;
132
-const SentCompletedShellId /* */ = 0b0100000;
133
-const SentMarkShellTime /* */ = 0b1000000;
127
+const NothingSent /* */ = 0b000000000;
128
+const SentCompleteSegmentFunction /* */ = 0b000000001;
129
+const SentCompleteBoundaryFunction /* */ = 0b000000010;
130
+const SentClientRenderFunction /* */ = 0b000000100;
131
+const SentStyleInsertionFunction /* */ = 0b000001000;
132
+const SentFormReplayingRuntime /* */ = 0b000010000;
133
+const SentCompletedShellId /* */ = 0b000100000;
134
+const SentMarkShellTime /* */ = 0b001000000;
135
+const NeedUpgradeToViewTransitions /* */ = 0b010000000;
136
+const SentUpgradeToViewTransitions /* */ = 0b100000000;
137
138
// Per request, global state that is not contextual to the rendering subtree.
139
// This cannot be resumed and therefore should only contain things that are
@@ -742,12 +745,13 @@ const HTML_COLGROUP_MODE = 9;
745
746
type InsertionMode = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
747
745
-const NO_SCOPE = /* */ 0b00000;
746
-const NOSCRIPT_SCOPE = /* */ 0b00001;
747
-const PICTURE_SCOPE = /* */ 0b00010;
748
-const FALLBACK_SCOPE = /* */ 0b00100;
749
-const EXIT_SCOPE = /* */ 0b01000; // A direct Instance below a Suspense fallback is the only thing that can "exit"
750
-const ENTER_SCOPE = /* */ 0b10000; // A direct Instance below Suspense content is the only thing that can "enter"
748
+const NO_SCOPE = /* */ 0b000000;
749
+const NOSCRIPT_SCOPE = /* */ 0b000001;
750
+const PICTURE_SCOPE = /* */ 0b000010;
751
+const FALLBACK_SCOPE = /* */ 0b000100;
752
+const EXIT_SCOPE = /* */ 0b001000; // A direct Instance below a Suspense fallback is the only thing that can "exit"
753
+const ENTER_SCOPE = /* */ 0b010000; // A direct Instance below Suspense content is the only thing that can "enter"
754
+const UPDATE_SCOPE = /* */ 0b100000; // Inside a scope that applies "update" ViewTransitions if anything mutates here.
755
756
// Everything not listed here are tracked for the whole subtree as opposed to just
757
// until the next Instance.
@@ -926,8 +930,15 @@ function getSuspenseViewTransition(
930
}
931
932
export function getSuspenseFallbackFormatContext(
933
+ resumableState: ResumableState,
934
parentContext: FormatContext,
935
): FormatContext {
936
+ if (parentContext.tagScope & UPDATE_SCOPE) {
937
+ // If we're rendering a Suspense in fallback mode and that is inside a ViewTransition,
938
+ // which hasn't disabled updates, then revealing it might animate the parent so we need
939
+ // the ViewTransition instructions.
940
+ resumableState.instructions |= NeedUpgradeToViewTransitions;
941
+ }
942
return createFormatContext(
943
parentContext.insertionMode,
944
parentContext.selectedValue,
@@ -937,6 +948,7 @@ export function getSuspenseFallbackFormatContext(
948
}
949
950
export function getSuspenseContentFormatContext(
951
+ resumableState: ResumableState,
952
parentContext: FormatContext,
953
): FormatContext {
954
return createFormatContext(
@@ -948,6 +960,7 @@ export function getSuspenseContentFormatContext(
960
}
961
962
export function getViewTransitionFormatContext(
963
+ resumableState: ResumableState,
964
parentContext: FormatContext,
965
update: ?string,
966
enter: ?string,
@@ -983,14 +996,26 @@ export function getViewTransitionFormatContext(
996
// exit because enter/exit will take precedence and if it's deeply nested
997
// it just animates along whatever the parent does when disabled.
998
share = null;
986
- } else if (share == null) {
987
- share = 'auto';
999
+ } else {
1000
+ if (share == null) {
1001
+ share = 'auto';
1002
+ }
1003
+ if (parentContext.tagScope & FALLBACK_SCOPE) {
1004
+ // If we have an explicit name and share is not disabled, and we're inside
1005
+ // a fallback, then that fallback might pair with content and so we might need
1006
+ // the ViewTransition instructions to animate between them.
1007
+ resumableState.instructions |= NeedUpgradeToViewTransitions;
1008
+ }
1009
}
1010
if (!(parentContext.tagScope & EXIT_SCOPE)) {
1011
exit = null; // exit is only relevant for the first ViewTransition inside fallback
1012
+ } else {
1013
+ resumableState.instructions |= NeedUpgradeToViewTransitions;
1014
}
1015
if (!(parentContext.tagScope & ENTER_SCOPE)) {
1016
enter = null; // enter is only relevant for the first ViewTransition inside content
1017
+ } else {
1018
+ resumableState.instructions |= NeedUpgradeToViewTransitions;
1019
}
1020
const viewTransition: ViewTransitionContext = {
1021
update,
@@ -1001,7 +1026,12 @@ export function getViewTransitionFormatContext(
1026
autoName,
1027
nameIdx: 0,
1028
};
1004
- const subtreeScope = parentContext.tagScope & SUBTREE_SCOPE;
1029
+ let subtreeScope = parentContext.tagScope & SUBTREE_SCOPE;
1030
+ if (update !== 'none') {
1031
+ subtreeScope |= UPDATE_SCOPE;
1032
+ } else {
1033
+ subtreeScope &= ~UPDATE_SCOPE;
1034
+ }
1035
return createFormatContext(
1036
parentContext.insertionMode,
1037
parentContext.selectedValue,
@@ -4780,9 +4810,8 @@ export function writeCompletedSegmentInstruction(
4810
const completeBoundaryScriptFunctionOnly = stringToPrecomputedChunk(
4811
completeBoundaryFunction,
4812
);
4783
-const completeBoundaryScript1Full = stringToPrecomputedChunk(
4784
- completeBoundaryFunction + '$RC("',
4785
-);
4813
+const completeBoundaryUpgradeToViewTransitionsInstruction =
4814
+ stringToPrecomputedChunk(upgradeToViewTransitionsInstruction);
4815
const completeBoundaryScript1Partial = stringToPrecomputedChunk('$RC("');
4816
4817
const completeBoundaryWithStylesScript1FullPartial = stringToPrecomputedChunk(
@@ -4814,6 +4843,10 @@ export function writeCompletedBoundaryInstruction(
4843
hoistableState: HoistableState,
4844
): boolean {
4845
const requiresStyleInsertion = renderState.stylesToHoist;
4846
+ const requiresViewTransitions =
4847
+ enableViewTransition &&
4848
+ (resumableState.instructions & NeedUpgradeToViewTransitions) !==
4849
+ NothingSent;
4850
// If necessary stylesheets will be flushed with this instruction.
4851
// Any style tags not yet hoisted in the Document will also be hoisted.
4852
// We reset this state since after this instruction executes all styles
@@ -4842,6 +4875,17 @@ export function writeCompletedBoundaryInstruction(
4875
resumableState.instructions |= SentCompleteBoundaryFunction;
4876
writeChunk(destination, completeBoundaryScriptFunctionOnly);
4877
}
4878
+ if (
4879
+ requiresViewTransitions &&
4880
+ (resumableState.instructions & SentUpgradeToViewTransitions) ===
4881
+ NothingSent
4882
+ ) {
4883
+ resumableState.instructions |= SentUpgradeToViewTransitions;
4884
+ writeChunk(
4885
+ destination,
4886
+ completeBoundaryUpgradeToViewTransitionsInstruction,
4887
+ );
4888
+ }
4889
if (
4890
(resumableState.instructions & SentStyleInsertionFunction) ===
4891
NothingSent
@@ -4857,10 +4901,20 @@ export function writeCompletedBoundaryInstruction(
4901
NothingSent
4902
) {
4903
resumableState.instructions |= SentCompleteBoundaryFunction;
4860
- writeChunk(destination, completeBoundaryScript1Full);
4861
- } else {
4862
- writeChunk(destination, completeBoundaryScript1Partial);
4904
+ writeChunk(destination, completeBoundaryScriptFunctionOnly);
4905
+ }
4906
+ if (
4907
+ requiresViewTransitions &&
4908
+ (resumableState.instructions & SentUpgradeToViewTransitions) ===
4909
+ NothingSent
4910
+ ) {
4911
+ resumableState.instructions |= SentUpgradeToViewTransitions;
4912
+ writeChunk(
4913
+ destination,
4914
+ completeBoundaryUpgradeToViewTransitionsInstruction,
4915
+ );
4916
}
4917
+ writeChunk(destination, completeBoundaryScript1Partial);
4918
}
4919
} else {
4920
if (requiresStyleInsertion) {
packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js
+1
@@ -181,6 +181,7 @@ export {
181
import escapeTextForBrowser from './escapeTextForBrowser';
182
183
export function getViewTransitionFormatContext(
184
+ resumableState: ResumableState,
185
parentContext: FormatContext,
186
update: void | null | 'none' | 'auto' | string,
187
enter: void | null | 'none' | 'auto' | string,
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteBoundary.js
+6
-1
@@ -1,7 +1,12 @@
1
-import {completeBoundary} from './ReactDOMFizzInstructionSetShared';
1
+import {
2
+ revealCompletedBoundaries,
3
+ completeBoundary,
4
+} from './ReactDOMFizzInstructionSetShared';
5
6
// This is a string so Closure's advanced compilation mode doesn't mangle it.
7
// eslint-disable-next-line dot-notation
8
window['$RB'] = [];
9
// eslint-disable-next-line dot-notation
10
+window['$RV'] = revealCompletedBoundaries;
11
+// eslint-disable-next-line dot-notation
12
window['$RC'] = completeBoundary;
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteBoundaryUpgradeToViewTransitions.js
new
+10
@@ -0,0 +1,10 @@
1
+import {revealCompletedBoundariesWithViewTransitions} from './ReactDOMFizzInstructionSetShared';
2
+
3
+// Upgrade the revealCompletedBoundaries instruction to support ViewTransitions.
4
+// This is a string so Closure's advanced compilation mode doesn't mangle it.
5
+// eslint-disable-next-line dot-notation
6
+window['$RV'] = revealCompletedBoundariesWithViewTransitions.bind(
7
+ null,
8
+ // eslint-disable-next-line dot-notation
9
+ window['$RV'],
10
+);
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetExternalRuntime.js
+6
@@ -8,6 +8,8 @@ import {
8
completeBoundaryWithStyles,
9
completeSegment,
10
listenToFormSubmissionsForReplaying,
11
+ revealCompletedBoundaries,
12
+ revealCompletedBoundariesWithViewTransitions,
13
} from './ReactDOMFizzInstructionSetShared';
14
15
// This is a string so Closure's advanced compilation mode doesn't mangle it.
@@ -15,6 +17,10 @@ import {
17
window['$RM'] = new Map();
18
window['$RB'] = [];
19
window['$RX'] = clientRenderBoundary;
20
+window['$RV'] = revealCompletedBoundariesWithViewTransitions.bind(
21
+ null,
22
+ revealCompletedBoundaries,
23
+);
24
window['$RC'] = completeBoundary;
25
window['$RR'] = completeBoundaryWithStyles;
26
window['$RS'] = completeSegment;
packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js
+3
-1
@@ -6,7 +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=[];$RC=function(d,c){function m(){$RT=performance.now();var f=$RB;$RB=[];for(var e=0;e<f.length;e+=2){var a=f[e],l=f[e+1],g=a.parentNode;if(g){var h=a.previousSibling,k=0;do{if(a&&8===a.nodeType){var b=a.data;if("/$"===b||"/&"===b)if(0===k)break;else k--;else"$"!==b&&"$?"!==b&&"$~"!==b&&"$!"!==b&&"&"!==b||k++}b=a.nextSibling;g.removeChild(a);a=b}while(a);for(;l.firstChild;)g.insertBefore(l.firstChild,a);h.data="$";h._reactRetry&&h._reactRetry()}}}if(c=document.getElementById(c))if(c.parentNode.removeChild(c),d=\ndocument.getElementById(d))d.previousSibling.data="$~",$RB.push(d,c),2===$RB.length&&setTimeout(m,("number"!==typeof $RT?0:$RT)+300-performance.now())};';
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())};';
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);';
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
+95
-64
@@ -18,6 +18,100 @@ 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() {
22
+ window['$RT'] = performance.now();
23
+ const batch = window['$RB'];
24
+ window['$RB'] = [];
25
+ for (let i = 0; i < batch.length; i += 2) {
26
+ const suspenseIdNode = batch[i];
27
+ const contentNode = batch[i + 1];
28
+
29
+ // Clear all the existing children. This is complicated because
30
+ // there can be embedded Suspense boundaries in the fallback.
31
+ // This is similar to clearSuspenseBoundary in ReactFiberConfigDOM.
32
+ // TODO: We could avoid this if we never emitted suspense boundaries in fallback trees.
33
+ // They never hydrate anyway. However, currently we support incrementally loading the fallback.
34
+ const parentInstance = suspenseIdNode.parentNode;
35
+ if (!parentInstance) {
36
+ // We may have client-rendered this boundary already. Skip it.
37
+ continue;
38
+ }
39
+
40
+ // Find the boundary around the fallback. This is always the previous node.
41
+ const suspenseNode = suspenseIdNode.previousSibling;
42
+
43
+ let node = suspenseIdNode;
44
+ let depth = 0;
45
+ do {
46
+ if (node && node.nodeType === COMMENT_NODE) {
47
+ const data = node.data;
48
+ if (data === SUSPENSE_END_DATA || data === ACTIVITY_END_DATA) {
49
+ if (depth === 0) {
50
+ break;
51
+ } else {
52
+ depth--;
53
+ }
54
+ } else if (
55
+ data === SUSPENSE_START_DATA ||
56
+ data === SUSPENSE_PENDING_START_DATA ||
57
+ data === SUSPENSE_QUEUED_START_DATA ||
58
+ data === SUSPENSE_FALLBACK_START_DATA ||
59
+ data === ACTIVITY_START_DATA
60
+ ) {
61
+ depth++;
62
+ }
63
+ }
64
+
65
+ const nextNode = node.nextSibling;
66
+ parentInstance.removeChild(node);
67
+ node = nextNode;
68
+ } while (node);
69
+
70
+ const endOfBoundary = node;
71
+
72
+ // Insert all the children from the contentNode between the start and end of suspense boundary.
73
+ while (contentNode.firstChild) {
74
+ parentInstance.insertBefore(contentNode.firstChild, endOfBoundary);
75
+ }
76
+
77
+ suspenseNode.data = SUSPENSE_START_DATA;
78
+ if (suspenseNode['_reactRetry']) {
79
+ suspenseNode['_reactRetry']();
80
+ }
81
+ }
82
+}
83
+
84
+export function revealCompletedBoundariesWithViewTransitions(revealBoundaries) {
85
+ try {
86
+ const existingTransition = document['__reactViewTransition'];
87
+ if (existingTransition) {
88
+ // Retry after the previous ViewTransition finishes.
89
+ existingTransition.finished.then(window['$RV'], window['$RV']);
90
+ return;
91
+ }
92
+ const shouldStartViewTransition = window['_useVT']; // TODO: Detect.
93
+ if (shouldStartViewTransition) {
94
+ const transition = (document['__reactViewTransition'] = document[
95
+ 'startViewTransition'
96
+ ]({
97
+ update: revealBoundaries,
98
+ types: [], // TODO: Add a hard coded type for Suspense reveals.
99
+ }));
100
+ transition.finished.finally(() => {
101
+ if (document['__reactViewTransition'] === transition) {
102
+ document['__reactViewTransition'] = null;
103
+ }
104
+ });
105
+ return;
106
+ }
107
+ // Fall through to reveal.
108
+ } catch (x) {
109
+ // Fall through to reveal.
110
+ }
111
+ // ViewTransitions v2 not supported or no ViewTransitions found. Reveal immediately.
112
+ revealBoundaries();
113
+}
114
+
115
export function clientRenderBoundary(
116
suspenseBoundaryID,
117
errorDigest,
@@ -71,69 +165,6 @@ export function completeBoundary(suspenseBoundaryID, contentID) {
165
return;
166
}
167
74
- function revealCompletedBoundaries() {
75
- window['$RT'] = performance.now();
76
- const batch = window['$RB'];
77
- window['$RB'] = [];
78
- for (let i = 0; i < batch.length; i += 2) {
79
- const suspenseIdNode = batch[i];
80
- const contentNode = batch[i + 1];
81
-
82
- // Clear all the existing children. This is complicated because
83
- // there can be embedded Suspense boundaries in the fallback.
84
- // This is similar to clearSuspenseBoundary in ReactFiberConfigDOM.
85
- // TODO: We could avoid this if we never emitted suspense boundaries in fallback trees.
86
- // They never hydrate anyway. However, currently we support incrementally loading the fallback.
87
- const parentInstance = suspenseIdNode.parentNode;
88
- if (!parentInstance) {
89
- // We may have client-rendered this boundary already. Skip it.
90
- continue;
91
- }
92
-
93
- // Find the boundary around the fallback. This is always the previous node.
94
- const suspenseNode = suspenseIdNode.previousSibling;
95
-
96
- let node = suspenseIdNode;
97
- let depth = 0;
98
- do {
99
- if (node && node.nodeType === COMMENT_NODE) {
100
- const data = node.data;
101
- if (data === SUSPENSE_END_DATA || data === ACTIVITY_END_DATA) {
102
- if (depth === 0) {
103
- break;
104
- } else {
105
- depth--;
106
- }
107
- } else if (
108
- data === SUSPENSE_START_DATA ||
109
- data === SUSPENSE_PENDING_START_DATA ||
110
- data === SUSPENSE_QUEUED_START_DATA ||
111
- data === SUSPENSE_FALLBACK_START_DATA ||
112
- data === ACTIVITY_START_DATA
113
- ) {
114
- depth++;
115
- }
116
- }
117
-
118
- const nextNode = node.nextSibling;
119
- parentInstance.removeChild(node);
120
- node = nextNode;
121
- } while (node);
122
-
123
- const endOfBoundary = node;
124
-
125
- // Insert all the children from the contentNode between the start and end of suspense boundary.
126
- while (contentNode.firstChild) {
127
- parentInstance.insertBefore(contentNode.firstChild, endOfBoundary);
128
- }
129
-
130
- suspenseNode.data = SUSPENSE_START_DATA;
131
- if (suspenseNode['_reactRetry']) {
132
- suspenseNode['_reactRetry']();
133
- }
134
- }
135
- }
136
-
168
// Mark this Suspense boundary as queued so we know not to client render it
169
// at the end of document load.
170
const suspenseNodeOuter = suspenseIdNodeOuter.previousSibling;
@@ -151,7 +182,7 @@ export function completeBoundary(suspenseBoundaryID, contentID) {
182
// We always schedule the flush in a timer even if it's very low or negative to allow
183
// for multiple completeBoundary calls that are already queued to have a chance to
184
// make the batch.
154
- setTimeout(revealCompletedBoundaries, msUntilTimeout);
185
+ setTimeout(window['$RV'], msUntilTimeout);
186
}
187
}
188
packages/react-markup/src/ReactFizzConfigMarkup.js
+1
@@ -89,6 +89,7 @@ export {
89
import escapeTextForBrowser from 'react-dom-bindings/src/server/escapeTextForBrowser';
90
91
export function getViewTransitionFormatContext(
92
+ resumableState: ResumableState,
93
parentContext: FormatContext,
94
update: void | null | 'none' | 'auto' | string,
95
enter: void | null | 'none' | 'auto' | string,
packages/react-server/src/ReactFizzServer.js
+29
-7
@@ -1146,7 +1146,10 @@ function renderSuspenseBoundary(
1146
const prevKeyPath = someTask.keyPath;
1147
const prevContext = someTask.formatContext;
1148
someTask.keyPath = keyPath;
1149
- someTask.formatContext = getSuspenseContentFormatContext(prevContext);
1149
+ someTask.formatContext = getSuspenseContentFormatContext(
1150
+ request.resumableState,
1151
+ prevContext,
1152
+ );
1153
const content: ReactNodeList = props.children;
1154
try {
1155
renderNode(request, someTask, content, -1);
@@ -1239,7 +1242,10 @@ function renderSuspenseBoundary(
1242
task.blockedSegment = boundarySegment;
1243
task.blockedPreamble = newBoundary.fallbackPreamble;
1244
task.keyPath = fallbackKeyPath;
1242
- task.formatContext = getSuspenseFallbackFormatContext(prevContext);
1245
+ task.formatContext = getSuspenseFallbackFormatContext(
1246
+ request.resumableState,
1247
+ prevContext,
1248
+ );
1249
boundarySegment.status = RENDERING;
1250
try {
1251
renderNode(request, task, fallback, -1);
@@ -1278,7 +1284,10 @@ function renderSuspenseBoundary(
1284
newBoundary.contentState,
1285
task.abortSet,
1286
keyPath,
1281
- getSuspenseContentFormatContext(task.formatContext),
1287
+ getSuspenseContentFormatContext(
1288
+ request.resumableState,
1289
+ task.formatContext,
1290
+ ),
1291
task.context,
1292
task.treeContext,
1293
task.componentStack,
@@ -1305,7 +1314,10 @@ function renderSuspenseBoundary(
1314
task.hoistableState = newBoundary.contentState;
1315
task.blockedSegment = contentRootSegment;
1316
task.keyPath = keyPath;
1308
- task.formatContext = getSuspenseContentFormatContext(prevContext);
1317
+ task.formatContext = getSuspenseContentFormatContext(
1318
+ request.resumableState,
1319
+ prevContext,
1320
+ );
1321
contentRootSegment.status = RENDERING;
1322
1323
try {
@@ -1409,7 +1421,10 @@ function renderSuspenseBoundary(
1421
newBoundary.fallbackState,
1422
fallbackAbortSet,
1423
fallbackKeyPath,
1412
- getSuspenseFallbackFormatContext(task.formatContext),
1424
+ getSuspenseFallbackFormatContext(
1425
+ request.resumableState,
1426
+ task.formatContext,
1427
+ ),
1428
task.context,
1429
task.treeContext,
1430
task.componentStack,
@@ -1471,7 +1486,10 @@ function replaySuspenseBoundary(
1486
task.blockedBoundary = resumedBoundary;
1487
task.hoistableState = resumedBoundary.contentState;
1488
task.keyPath = keyPath;
1474
- task.formatContext = getSuspenseContentFormatContext(prevContext);
1489
+ task.formatContext = getSuspenseContentFormatContext(
1490
+ request.resumableState,
1491
+ prevContext,
1492
+ );
1493
task.replay = {nodes: childNodes, slots: childSlots, pendingTasks: 1};
1494
1495
try {
@@ -1569,7 +1587,10 @@ function replaySuspenseBoundary(
1587
resumedBoundary.fallbackState,
1588
fallbackAbortSet,
1589
fallbackKeyPath,
1572
- getSuspenseFallbackFormatContext(task.formatContext),
1590
+ getSuspenseFallbackFormatContext(
1591
+ request.resumableState,
1592
+ task.formatContext,
1593
+ ),
1594
task.context,
1595
task.treeContext,
1596
task.componentStack,
@@ -2284,6 +2305,7 @@ function renderViewTransition(
2305
request.resumableState,
2306
);
2307
task.formatContext = getViewTransitionFormatContext(
2308
+ request.resumableState,
2309
prevContext,
2310
getViewTransitionClassName(props.default, props.update),
2311
getViewTransitionClassName(props.default, props.enter),
scripts/rollup/generate-inline-fizz-runtime.js
+4
@@ -25,6 +25,10 @@ const config = [
25
entry: 'ReactDOMFizzInlineCompleteBoundary.js',
26
exportName: 'completeBoundary',
27
},
28
+ {
29
+ entry: 'ReactDOMFizzInlineCompleteBoundaryUpgradeToViewTransitions.js',
30
+ exportName: 'completeBoundaryUpgradeToViewTransitions',
31
+ },
32
{
33
entry: 'ReactDOMFizzInlineCompleteBoundaryWithStyles.js',
34
exportName: 'completeBoundaryWithStyles',