@samitouri / QOS-React-1 / commits / 6060367ef8

[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',