Skip to content

Commit 6060367

Browse files
authored
[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.
1 parent c250b7d commit 6060367

File tree

12 files changed

+259
-109
lines changed

12 files changed

+259
-109
lines changed

fixtures/view-transition/server/render.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export default function render(url, res) {
2323
const {pipe, abort} = renderToPipeableStream(
2424
<App assets={assets} initialURL={url} />,
2525
{
26+
// TODO: Temporary hack. Detect from attributes instead.
27+
bootstrapScriptContent: 'window._useVT = true;',
2628
bootstrapScripts: [assets['main.js']],
2729
onShellReady() {
2830
// If something errored before we started streaming, we set the error code appropriately.

fixtures/view-transition/src/components/Page.js

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import React, {
88
useId,
99
useOptimistic,
1010
startTransition,
11+
Suspense,
1112
} from 'react';
1213

1314
import {createPortal} from 'react-dom';
@@ -60,6 +61,12 @@ function Id() {
6061
return <span id={useId()} />;
6162
}
6263

64+
let wait;
65+
function Suspend() {
66+
if (!wait) wait = sleep(500);
67+
return React.use(wait);
68+
}
69+
6370
export default function Page({url, navigate}) {
6471
const [renderedUrl, optimisticNavigate] = useOptimistic(
6572
url,
@@ -93,7 +100,7 @@ export default function Page({url, navigate}) {
93100
// a flushSync will.
94101
// Promise.resolve().then(() => {
95102
// flushSync(() => {
96-
setCounter(c => c + 10);
103+
// setCounter(c => c + 10);
97104
// });
98105
// });
99106
}, [show]);
@@ -193,18 +200,23 @@ export default function Page({url, navigate}) {
193200
<div>!!</div>
194201
</ViewTransition>
195202
</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>
208220
{show ? <Component /> : null}
209221
</div>
210222
</ViewTransition>

packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js

Lines changed: 77 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ import isArray from 'shared/isArray';
8080
import {
8181
clientRenderBoundary as clientRenderFunction,
8282
completeBoundary as completeBoundaryFunction,
83+
completeBoundaryUpgradeToViewTransitions as upgradeToViewTransitionsInstruction,
8384
completeBoundaryWithStyles as styleInsertionFunction,
8485
completeSegment as completeSegmentFunction,
8586
formReplaying as formReplayingRuntime,
@@ -123,14 +124,16 @@ const ScriptStreamingFormat: StreamingFormat = 0;
123124
const DataStreamingFormat: StreamingFormat = 1;
124125

125126
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;
134137

135138
// Per request, global state that is not contextual to the rendering subtree.
136139
// This cannot be resumed and therefore should only contain things that are
@@ -742,12 +745,13 @@ const HTML_COLGROUP_MODE = 9;
742745

743746
type InsertionMode = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
744747

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.
751755

752756
// Everything not listed here are tracked for the whole subtree as opposed to just
753757
// until the next Instance.
@@ -926,8 +930,15 @@ function getSuspenseViewTransition(
926930
}
927931

928932
export function getSuspenseFallbackFormatContext(
933+
resumableState: ResumableState,
929934
parentContext: FormatContext,
930935
): 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+
}
931942
return createFormatContext(
932943
parentContext.insertionMode,
933944
parentContext.selectedValue,
@@ -937,6 +948,7 @@ export function getSuspenseFallbackFormatContext(
937948
}
938949

939950
export function getSuspenseContentFormatContext(
951+
resumableState: ResumableState,
940952
parentContext: FormatContext,
941953
): FormatContext {
942954
return createFormatContext(
@@ -948,6 +960,7 @@ export function getSuspenseContentFormatContext(
948960
}
949961

950962
export function getViewTransitionFormatContext(
963+
resumableState: ResumableState,
951964
parentContext: FormatContext,
952965
update: ?string,
953966
enter: ?string,
@@ -983,14 +996,26 @@ export function getViewTransitionFormatContext(
983996
// exit because enter/exit will take precedence and if it's deeply nested
984997
// it just animates along whatever the parent does when disabled.
985998
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+
}
9881009
}
9891010
if (!(parentContext.tagScope & EXIT_SCOPE)) {
9901011
exit = null; // exit is only relevant for the first ViewTransition inside fallback
1012+
} else {
1013+
resumableState.instructions |= NeedUpgradeToViewTransitions;
9911014
}
9921015
if (!(parentContext.tagScope & ENTER_SCOPE)) {
9931016
enter = null; // enter is only relevant for the first ViewTransition inside content
1017+
} else {
1018+
resumableState.instructions |= NeedUpgradeToViewTransitions;
9941019
}
9951020
const viewTransition: ViewTransitionContext = {
9961021
update,
@@ -1001,7 +1026,12 @@ export function getViewTransitionFormatContext(
10011026
autoName,
10021027
nameIdx: 0,
10031028
};
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+
}
10051035
return createFormatContext(
10061036
parentContext.insertionMode,
10071037
parentContext.selectedValue,
@@ -4780,9 +4810,8 @@ export function writeCompletedSegmentInstruction(
47804810
const completeBoundaryScriptFunctionOnly = stringToPrecomputedChunk(
47814811
completeBoundaryFunction,
47824812
);
4783-
const completeBoundaryScript1Full = stringToPrecomputedChunk(
4784-
completeBoundaryFunction + '$RC("',
4785-
);
4813+
const completeBoundaryUpgradeToViewTransitionsInstruction =
4814+
stringToPrecomputedChunk(upgradeToViewTransitionsInstruction);
47864815
const completeBoundaryScript1Partial = stringToPrecomputedChunk('$RC("');
47874816

47884817
const completeBoundaryWithStylesScript1FullPartial = stringToPrecomputedChunk(
@@ -4814,6 +4843,10 @@ export function writeCompletedBoundaryInstruction(
48144843
hoistableState: HoistableState,
48154844
): boolean {
48164845
const requiresStyleInsertion = renderState.stylesToHoist;
4846+
const requiresViewTransitions =
4847+
enableViewTransition &&
4848+
(resumableState.instructions & NeedUpgradeToViewTransitions) !==
4849+
NothingSent;
48174850
// If necessary stylesheets will be flushed with this instruction.
48184851
// Any style tags not yet hoisted in the Document will also be hoisted.
48194852
// We reset this state since after this instruction executes all styles
@@ -4842,6 +4875,17 @@ export function writeCompletedBoundaryInstruction(
48424875
resumableState.instructions |= SentCompleteBoundaryFunction;
48434876
writeChunk(destination, completeBoundaryScriptFunctionOnly);
48444877
}
4878+
if (
4879+
requiresViewTransitions &&
4880+
(resumableState.instructions & SentUpgradeToViewTransitions) ===
4881+
NothingSent
4882+
) {
4883+
resumableState.instructions |= SentUpgradeToViewTransitions;
4884+
writeChunk(
4885+
destination,
4886+
completeBoundaryUpgradeToViewTransitionsInstruction,
4887+
);
4888+
}
48454889
if (
48464890
(resumableState.instructions & SentStyleInsertionFunction) ===
48474891
NothingSent
@@ -4857,10 +4901,20 @@ export function writeCompletedBoundaryInstruction(
48574901
NothingSent
48584902
) {
48594903
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+
);
48634916
}
4917+
writeChunk(destination, completeBoundaryScript1Partial);
48644918
}
48654919
} else {
48664920
if (requiresStyleInsertion) {

packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ export {
181181
import escapeTextForBrowser from './escapeTextForBrowser';
182182

183183
export function getViewTransitionFormatContext(
184+
resumableState: ResumableState,
184185
parentContext: FormatContext,
185186
update: void | null | 'none' | 'auto' | string,
186187
enter: void | null | 'none' | 'auto' | string,
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1-
import {completeBoundary} from './ReactDOMFizzInstructionSetShared';
1+
import {
2+
revealCompletedBoundaries,
3+
completeBoundary,
4+
} from './ReactDOMFizzInstructionSetShared';
25

36
// This is a string so Closure's advanced compilation mode doesn't mangle it.
47
// eslint-disable-next-line dot-notation
58
window['$RB'] = [];
69
// eslint-disable-next-line dot-notation
10+
window['$RV'] = revealCompletedBoundaries;
11+
// eslint-disable-next-line dot-notation
712
window['$RC'] = completeBoundary;
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,19 @@ import {
88
completeBoundaryWithStyles,
99
completeSegment,
1010
listenToFormSubmissionsForReplaying,
11+
revealCompletedBoundaries,
12+
revealCompletedBoundariesWithViewTransitions,
1113
} from './ReactDOMFizzInstructionSetShared';
1214

1315
// This is a string so Closure's advanced compilation mode doesn't mangle it.
1416
// These will be renamed to local references by the external-runtime-plugin.
1517
window['$RM'] = new Map();
1618
window['$RB'] = [];
1719
window['$RX'] = clientRenderBoundary;
20+
window['$RV'] = revealCompletedBoundariesWithViewTransitions.bind(
21+
null,
22+
revealCompletedBoundaries,
23+
);
1824
window['$RC'] = completeBoundary;
1925
window['$RR'] = completeBoundaryWithStyles;
2026
window['$RS'] = completeSegment;

packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)