mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 00:20:04 +01:00
[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.
This commit is contained in:
parent
c250b7d980
commit
6060367ef8
|
|
@ -23,6 +23,8 @@ export default function render(url, res) {
|
|||
const {pipe, abort} = renderToPipeableStream(
|
||||
<App assets={assets} initialURL={url} />,
|
||||
{
|
||||
// TODO: Temporary hack. Detect from attributes instead.
|
||||
bootstrapScriptContent: 'window._useVT = true;',
|
||||
bootstrapScripts: [assets['main.js']],
|
||||
onShellReady() {
|
||||
// If something errored before we started streaming, we set the error code appropriately.
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import React, {
|
|||
useId,
|
||||
useOptimistic,
|
||||
startTransition,
|
||||
Suspense,
|
||||
} from 'react';
|
||||
|
||||
import {createPortal} from 'react-dom';
|
||||
|
|
@ -60,6 +61,12 @@ function Id() {
|
|||
return <span id={useId()} />;
|
||||
}
|
||||
|
||||
let wait;
|
||||
function Suspend() {
|
||||
if (!wait) wait = sleep(500);
|
||||
return React.use(wait);
|
||||
}
|
||||
|
||||
export default function Page({url, navigate}) {
|
||||
const [renderedUrl, optimisticNavigate] = useOptimistic(
|
||||
url,
|
||||
|
|
@ -93,7 +100,7 @@ export default function Page({url, navigate}) {
|
|||
// a flushSync will.
|
||||
// Promise.resolve().then(() => {
|
||||
// flushSync(() => {
|
||||
setCounter(c => c + 10);
|
||||
// setCounter(c => c + 10);
|
||||
// });
|
||||
// });
|
||||
}, [show]);
|
||||
|
|
@ -193,18 +200,23 @@ export default function Page({url, navigate}) {
|
|||
<div>!!</div>
|
||||
</ViewTransition>
|
||||
</Activity>
|
||||
<p>these</p>
|
||||
<p>rows</p>
|
||||
<p>exist</p>
|
||||
<p>to</p>
|
||||
<p>test</p>
|
||||
<p>scrolling</p>
|
||||
<p>content</p>
|
||||
<p>out</p>
|
||||
<p>of</p>
|
||||
{portal}
|
||||
<p>the</p>
|
||||
<p>viewport</p>
|
||||
<Suspense fallback="Loading">
|
||||
<ViewTransition>
|
||||
<p>these</p>
|
||||
<p>rows</p>
|
||||
<p>exist</p>
|
||||
<p>to</p>
|
||||
<p>test</p>
|
||||
<p>scrolling</p>
|
||||
<p>content</p>
|
||||
<p>out</p>
|
||||
<p>of</p>
|
||||
{portal}
|
||||
<p>the</p>
|
||||
<p>viewport</p>
|
||||
<Suspend />
|
||||
</ViewTransition>
|
||||
</Suspense>
|
||||
{show ? <Component /> : null}
|
||||
</div>
|
||||
</ViewTransition>
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ import isArray from 'shared/isArray';
|
|||
import {
|
||||
clientRenderBoundary as clientRenderFunction,
|
||||
completeBoundary as completeBoundaryFunction,
|
||||
completeBoundaryUpgradeToViewTransitions as upgradeToViewTransitionsInstruction,
|
||||
completeBoundaryWithStyles as styleInsertionFunction,
|
||||
completeSegment as completeSegmentFunction,
|
||||
formReplaying as formReplayingRuntime,
|
||||
|
|
@ -123,14 +124,16 @@ const ScriptStreamingFormat: StreamingFormat = 0;
|
|||
const DataStreamingFormat: StreamingFormat = 1;
|
||||
|
||||
export type InstructionState = number;
|
||||
const NothingSent /* */ = 0b0000000;
|
||||
const SentCompleteSegmentFunction /* */ = 0b0000001;
|
||||
const SentCompleteBoundaryFunction /* */ = 0b0000010;
|
||||
const SentClientRenderFunction /* */ = 0b0000100;
|
||||
const SentStyleInsertionFunction /* */ = 0b0001000;
|
||||
const SentFormReplayingRuntime /* */ = 0b0010000;
|
||||
const SentCompletedShellId /* */ = 0b0100000;
|
||||
const SentMarkShellTime /* */ = 0b1000000;
|
||||
const NothingSent /* */ = 0b000000000;
|
||||
const SentCompleteSegmentFunction /* */ = 0b000000001;
|
||||
const SentCompleteBoundaryFunction /* */ = 0b000000010;
|
||||
const SentClientRenderFunction /* */ = 0b000000100;
|
||||
const SentStyleInsertionFunction /* */ = 0b000001000;
|
||||
const SentFormReplayingRuntime /* */ = 0b000010000;
|
||||
const SentCompletedShellId /* */ = 0b000100000;
|
||||
const SentMarkShellTime /* */ = 0b001000000;
|
||||
const NeedUpgradeToViewTransitions /* */ = 0b010000000;
|
||||
const SentUpgradeToViewTransitions /* */ = 0b100000000;
|
||||
|
||||
// Per request, global state that is not contextual to the rendering subtree.
|
||||
// This cannot be resumed and therefore should only contain things that are
|
||||
|
|
@ -742,12 +745,13 @@ const HTML_COLGROUP_MODE = 9;
|
|||
|
||||
type InsertionMode = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
|
||||
|
||||
const NO_SCOPE = /* */ 0b00000;
|
||||
const NOSCRIPT_SCOPE = /* */ 0b00001;
|
||||
const PICTURE_SCOPE = /* */ 0b00010;
|
||||
const FALLBACK_SCOPE = /* */ 0b00100;
|
||||
const EXIT_SCOPE = /* */ 0b01000; // A direct Instance below a Suspense fallback is the only thing that can "exit"
|
||||
const ENTER_SCOPE = /* */ 0b10000; // A direct Instance below Suspense content is the only thing that can "enter"
|
||||
const NO_SCOPE = /* */ 0b000000;
|
||||
const NOSCRIPT_SCOPE = /* */ 0b000001;
|
||||
const PICTURE_SCOPE = /* */ 0b000010;
|
||||
const FALLBACK_SCOPE = /* */ 0b000100;
|
||||
const EXIT_SCOPE = /* */ 0b001000; // A direct Instance below a Suspense fallback is the only thing that can "exit"
|
||||
const ENTER_SCOPE = /* */ 0b010000; // A direct Instance below Suspense content is the only thing that can "enter"
|
||||
const UPDATE_SCOPE = /* */ 0b100000; // Inside a scope that applies "update" ViewTransitions if anything mutates here.
|
||||
|
||||
// Everything not listed here are tracked for the whole subtree as opposed to just
|
||||
// until the next Instance.
|
||||
|
|
@ -926,8 +930,15 @@ function getSuspenseViewTransition(
|
|||
}
|
||||
|
||||
export function getSuspenseFallbackFormatContext(
|
||||
resumableState: ResumableState,
|
||||
parentContext: FormatContext,
|
||||
): FormatContext {
|
||||
if (parentContext.tagScope & UPDATE_SCOPE) {
|
||||
// If we're rendering a Suspense in fallback mode and that is inside a ViewTransition,
|
||||
// which hasn't disabled updates, then revealing it might animate the parent so we need
|
||||
// the ViewTransition instructions.
|
||||
resumableState.instructions |= NeedUpgradeToViewTransitions;
|
||||
}
|
||||
return createFormatContext(
|
||||
parentContext.insertionMode,
|
||||
parentContext.selectedValue,
|
||||
|
|
@ -937,6 +948,7 @@ export function getSuspenseFallbackFormatContext(
|
|||
}
|
||||
|
||||
export function getSuspenseContentFormatContext(
|
||||
resumableState: ResumableState,
|
||||
parentContext: FormatContext,
|
||||
): FormatContext {
|
||||
return createFormatContext(
|
||||
|
|
@ -948,6 +960,7 @@ export function getSuspenseContentFormatContext(
|
|||
}
|
||||
|
||||
export function getViewTransitionFormatContext(
|
||||
resumableState: ResumableState,
|
||||
parentContext: FormatContext,
|
||||
update: ?string,
|
||||
enter: ?string,
|
||||
|
|
@ -983,14 +996,26 @@ export function getViewTransitionFormatContext(
|
|||
// exit because enter/exit will take precedence and if it's deeply nested
|
||||
// it just animates along whatever the parent does when disabled.
|
||||
share = null;
|
||||
} else if (share == null) {
|
||||
share = 'auto';
|
||||
} else {
|
||||
if (share == null) {
|
||||
share = 'auto';
|
||||
}
|
||||
if (parentContext.tagScope & FALLBACK_SCOPE) {
|
||||
// If we have an explicit name and share is not disabled, and we're inside
|
||||
// a fallback, then that fallback might pair with content and so we might need
|
||||
// the ViewTransition instructions to animate between them.
|
||||
resumableState.instructions |= NeedUpgradeToViewTransitions;
|
||||
}
|
||||
}
|
||||
if (!(parentContext.tagScope & EXIT_SCOPE)) {
|
||||
exit = null; // exit is only relevant for the first ViewTransition inside fallback
|
||||
} else {
|
||||
resumableState.instructions |= NeedUpgradeToViewTransitions;
|
||||
}
|
||||
if (!(parentContext.tagScope & ENTER_SCOPE)) {
|
||||
enter = null; // enter is only relevant for the first ViewTransition inside content
|
||||
} else {
|
||||
resumableState.instructions |= NeedUpgradeToViewTransitions;
|
||||
}
|
||||
const viewTransition: ViewTransitionContext = {
|
||||
update,
|
||||
|
|
@ -1001,7 +1026,12 @@ export function getViewTransitionFormatContext(
|
|||
autoName,
|
||||
nameIdx: 0,
|
||||
};
|
||||
const subtreeScope = parentContext.tagScope & SUBTREE_SCOPE;
|
||||
let subtreeScope = parentContext.tagScope & SUBTREE_SCOPE;
|
||||
if (update !== 'none') {
|
||||
subtreeScope |= UPDATE_SCOPE;
|
||||
} else {
|
||||
subtreeScope &= ~UPDATE_SCOPE;
|
||||
}
|
||||
return createFormatContext(
|
||||
parentContext.insertionMode,
|
||||
parentContext.selectedValue,
|
||||
|
|
@ -4780,9 +4810,8 @@ export function writeCompletedSegmentInstruction(
|
|||
const completeBoundaryScriptFunctionOnly = stringToPrecomputedChunk(
|
||||
completeBoundaryFunction,
|
||||
);
|
||||
const completeBoundaryScript1Full = stringToPrecomputedChunk(
|
||||
completeBoundaryFunction + '$RC("',
|
||||
);
|
||||
const completeBoundaryUpgradeToViewTransitionsInstruction =
|
||||
stringToPrecomputedChunk(upgradeToViewTransitionsInstruction);
|
||||
const completeBoundaryScript1Partial = stringToPrecomputedChunk('$RC("');
|
||||
|
||||
const completeBoundaryWithStylesScript1FullPartial = stringToPrecomputedChunk(
|
||||
|
|
@ -4814,6 +4843,10 @@ export function writeCompletedBoundaryInstruction(
|
|||
hoistableState: HoistableState,
|
||||
): boolean {
|
||||
const requiresStyleInsertion = renderState.stylesToHoist;
|
||||
const requiresViewTransitions =
|
||||
enableViewTransition &&
|
||||
(resumableState.instructions & NeedUpgradeToViewTransitions) !==
|
||||
NothingSent;
|
||||
// If necessary stylesheets will be flushed with this instruction.
|
||||
// Any style tags not yet hoisted in the Document will also be hoisted.
|
||||
// We reset this state since after this instruction executes all styles
|
||||
|
|
@ -4842,6 +4875,17 @@ export function writeCompletedBoundaryInstruction(
|
|||
resumableState.instructions |= SentCompleteBoundaryFunction;
|
||||
writeChunk(destination, completeBoundaryScriptFunctionOnly);
|
||||
}
|
||||
if (
|
||||
requiresViewTransitions &&
|
||||
(resumableState.instructions & SentUpgradeToViewTransitions) ===
|
||||
NothingSent
|
||||
) {
|
||||
resumableState.instructions |= SentUpgradeToViewTransitions;
|
||||
writeChunk(
|
||||
destination,
|
||||
completeBoundaryUpgradeToViewTransitionsInstruction,
|
||||
);
|
||||
}
|
||||
if (
|
||||
(resumableState.instructions & SentStyleInsertionFunction) ===
|
||||
NothingSent
|
||||
|
|
@ -4857,10 +4901,20 @@ export function writeCompletedBoundaryInstruction(
|
|||
NothingSent
|
||||
) {
|
||||
resumableState.instructions |= SentCompleteBoundaryFunction;
|
||||
writeChunk(destination, completeBoundaryScript1Full);
|
||||
} else {
|
||||
writeChunk(destination, completeBoundaryScript1Partial);
|
||||
writeChunk(destination, completeBoundaryScriptFunctionOnly);
|
||||
}
|
||||
if (
|
||||
requiresViewTransitions &&
|
||||
(resumableState.instructions & SentUpgradeToViewTransitions) ===
|
||||
NothingSent
|
||||
) {
|
||||
resumableState.instructions |= SentUpgradeToViewTransitions;
|
||||
writeChunk(
|
||||
destination,
|
||||
completeBoundaryUpgradeToViewTransitionsInstruction,
|
||||
);
|
||||
}
|
||||
writeChunk(destination, completeBoundaryScript1Partial);
|
||||
}
|
||||
} else {
|
||||
if (requiresStyleInsertion) {
|
||||
|
|
|
|||
|
|
@ -181,6 +181,7 @@ export {
|
|||
import escapeTextForBrowser from './escapeTextForBrowser';
|
||||
|
||||
export function getViewTransitionFormatContext(
|
||||
resumableState: ResumableState,
|
||||
parentContext: FormatContext,
|
||||
update: void | null | 'none' | 'auto' | string,
|
||||
enter: void | null | 'none' | 'auto' | string,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
import {completeBoundary} from './ReactDOMFizzInstructionSetShared';
|
||||
import {
|
||||
revealCompletedBoundaries,
|
||||
completeBoundary,
|
||||
} from './ReactDOMFizzInstructionSetShared';
|
||||
|
||||
// This is a string so Closure's advanced compilation mode doesn't mangle it.
|
||||
// eslint-disable-next-line dot-notation
|
||||
window['$RB'] = [];
|
||||
// eslint-disable-next-line dot-notation
|
||||
window['$RV'] = revealCompletedBoundaries;
|
||||
// eslint-disable-next-line dot-notation
|
||||
window['$RC'] = completeBoundary;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
import {revealCompletedBoundariesWithViewTransitions} from './ReactDOMFizzInstructionSetShared';
|
||||
|
||||
// Upgrade the revealCompletedBoundaries instruction to support ViewTransitions.
|
||||
// This is a string so Closure's advanced compilation mode doesn't mangle it.
|
||||
// eslint-disable-next-line dot-notation
|
||||
window['$RV'] = revealCompletedBoundariesWithViewTransitions.bind(
|
||||
null,
|
||||
// eslint-disable-next-line dot-notation
|
||||
window['$RV'],
|
||||
);
|
||||
|
|
@ -8,6 +8,8 @@ import {
|
|||
completeBoundaryWithStyles,
|
||||
completeSegment,
|
||||
listenToFormSubmissionsForReplaying,
|
||||
revealCompletedBoundaries,
|
||||
revealCompletedBoundariesWithViewTransitions,
|
||||
} from './ReactDOMFizzInstructionSetShared';
|
||||
|
||||
// This is a string so Closure's advanced compilation mode doesn't mangle it.
|
||||
|
|
@ -15,6 +17,10 @@ import {
|
|||
window['$RM'] = new Map();
|
||||
window['$RB'] = [];
|
||||
window['$RX'] = clientRenderBoundary;
|
||||
window['$RV'] = revealCompletedBoundariesWithViewTransitions.bind(
|
||||
null,
|
||||
revealCompletedBoundaries,
|
||||
);
|
||||
window['$RC'] = completeBoundary;
|
||||
window['$RR'] = completeBoundaryWithStyles;
|
||||
window['$RS'] = completeSegment;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@ 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=[];$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())};';
|
||||
'$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())};';
|
||||
export const completeBoundaryUpgradeToViewTransitions =
|
||||
'$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);';
|
||||
export const completeBoundaryWithStyles =
|
||||
'$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"))};';
|
||||
export const completeSegment =
|
||||
|
|
|
|||
|
|
@ -18,6 +18,100 @@ const SUSPENSE_FALLBACK_START_DATA = '$!';
|
|||
// working. Closure converts it to a dot access anyway, though, so it's not an
|
||||
// urgent issue.
|
||||
|
||||
export function revealCompletedBoundaries() {
|
||||
window['$RT'] = performance.now();
|
||||
const batch = window['$RB'];
|
||||
window['$RB'] = [];
|
||||
for (let i = 0; i < batch.length; i += 2) {
|
||||
const suspenseIdNode = batch[i];
|
||||
const contentNode = batch[i + 1];
|
||||
|
||||
// Clear all the existing children. This is complicated because
|
||||
// there can be embedded Suspense boundaries in the fallback.
|
||||
// This is similar to clearSuspenseBoundary in ReactFiberConfigDOM.
|
||||
// TODO: We could avoid this if we never emitted suspense boundaries in fallback trees.
|
||||
// They never hydrate anyway. However, currently we support incrementally loading the fallback.
|
||||
const parentInstance = suspenseIdNode.parentNode;
|
||||
if (!parentInstance) {
|
||||
// We may have client-rendered this boundary already. Skip it.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the boundary around the fallback. This is always the previous node.
|
||||
const suspenseNode = suspenseIdNode.previousSibling;
|
||||
|
||||
let node = suspenseIdNode;
|
||||
let depth = 0;
|
||||
do {
|
||||
if (node && node.nodeType === COMMENT_NODE) {
|
||||
const data = node.data;
|
||||
if (data === SUSPENSE_END_DATA || data === ACTIVITY_END_DATA) {
|
||||
if (depth === 0) {
|
||||
break;
|
||||
} else {
|
||||
depth--;
|
||||
}
|
||||
} else if (
|
||||
data === SUSPENSE_START_DATA ||
|
||||
data === SUSPENSE_PENDING_START_DATA ||
|
||||
data === SUSPENSE_QUEUED_START_DATA ||
|
||||
data === SUSPENSE_FALLBACK_START_DATA ||
|
||||
data === ACTIVITY_START_DATA
|
||||
) {
|
||||
depth++;
|
||||
}
|
||||
}
|
||||
|
||||
const nextNode = node.nextSibling;
|
||||
parentInstance.removeChild(node);
|
||||
node = nextNode;
|
||||
} while (node);
|
||||
|
||||
const endOfBoundary = node;
|
||||
|
||||
// Insert all the children from the contentNode between the start and end of suspense boundary.
|
||||
while (contentNode.firstChild) {
|
||||
parentInstance.insertBefore(contentNode.firstChild, endOfBoundary);
|
||||
}
|
||||
|
||||
suspenseNode.data = SUSPENSE_START_DATA;
|
||||
if (suspenseNode['_reactRetry']) {
|
||||
suspenseNode['_reactRetry']();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function revealCompletedBoundariesWithViewTransitions(revealBoundaries) {
|
||||
try {
|
||||
const existingTransition = document['__reactViewTransition'];
|
||||
if (existingTransition) {
|
||||
// Retry after the previous ViewTransition finishes.
|
||||
existingTransition.finished.then(window['$RV'], window['$RV']);
|
||||
return;
|
||||
}
|
||||
const shouldStartViewTransition = window['_useVT']; // TODO: Detect.
|
||||
if (shouldStartViewTransition) {
|
||||
const transition = (document['__reactViewTransition'] = document[
|
||||
'startViewTransition'
|
||||
]({
|
||||
update: revealBoundaries,
|
||||
types: [], // TODO: Add a hard coded type for Suspense reveals.
|
||||
}));
|
||||
transition.finished.finally(() => {
|
||||
if (document['__reactViewTransition'] === transition) {
|
||||
document['__reactViewTransition'] = null;
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Fall through to reveal.
|
||||
} catch (x) {
|
||||
// Fall through to reveal.
|
||||
}
|
||||
// ViewTransitions v2 not supported or no ViewTransitions found. Reveal immediately.
|
||||
revealBoundaries();
|
||||
}
|
||||
|
||||
export function clientRenderBoundary(
|
||||
suspenseBoundaryID,
|
||||
errorDigest,
|
||||
|
|
@ -71,69 +165,6 @@ export function completeBoundary(suspenseBoundaryID, contentID) {
|
|||
return;
|
||||
}
|
||||
|
||||
function revealCompletedBoundaries() {
|
||||
window['$RT'] = performance.now();
|
||||
const batch = window['$RB'];
|
||||
window['$RB'] = [];
|
||||
for (let i = 0; i < batch.length; i += 2) {
|
||||
const suspenseIdNode = batch[i];
|
||||
const contentNode = batch[i + 1];
|
||||
|
||||
// Clear all the existing children. This is complicated because
|
||||
// there can be embedded Suspense boundaries in the fallback.
|
||||
// This is similar to clearSuspenseBoundary in ReactFiberConfigDOM.
|
||||
// TODO: We could avoid this if we never emitted suspense boundaries in fallback trees.
|
||||
// They never hydrate anyway. However, currently we support incrementally loading the fallback.
|
||||
const parentInstance = suspenseIdNode.parentNode;
|
||||
if (!parentInstance) {
|
||||
// We may have client-rendered this boundary already. Skip it.
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the boundary around the fallback. This is always the previous node.
|
||||
const suspenseNode = suspenseIdNode.previousSibling;
|
||||
|
||||
let node = suspenseIdNode;
|
||||
let depth = 0;
|
||||
do {
|
||||
if (node && node.nodeType === COMMENT_NODE) {
|
||||
const data = node.data;
|
||||
if (data === SUSPENSE_END_DATA || data === ACTIVITY_END_DATA) {
|
||||
if (depth === 0) {
|
||||
break;
|
||||
} else {
|
||||
depth--;
|
||||
}
|
||||
} else if (
|
||||
data === SUSPENSE_START_DATA ||
|
||||
data === SUSPENSE_PENDING_START_DATA ||
|
||||
data === SUSPENSE_QUEUED_START_DATA ||
|
||||
data === SUSPENSE_FALLBACK_START_DATA ||
|
||||
data === ACTIVITY_START_DATA
|
||||
) {
|
||||
depth++;
|
||||
}
|
||||
}
|
||||
|
||||
const nextNode = node.nextSibling;
|
||||
parentInstance.removeChild(node);
|
||||
node = nextNode;
|
||||
} while (node);
|
||||
|
||||
const endOfBoundary = node;
|
||||
|
||||
// Insert all the children from the contentNode between the start and end of suspense boundary.
|
||||
while (contentNode.firstChild) {
|
||||
parentInstance.insertBefore(contentNode.firstChild, endOfBoundary);
|
||||
}
|
||||
|
||||
suspenseNode.data = SUSPENSE_START_DATA;
|
||||
if (suspenseNode['_reactRetry']) {
|
||||
suspenseNode['_reactRetry']();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mark this Suspense boundary as queued so we know not to client render it
|
||||
// at the end of document load.
|
||||
const suspenseNodeOuter = suspenseIdNodeOuter.previousSibling;
|
||||
|
|
@ -151,7 +182,7 @@ export function completeBoundary(suspenseBoundaryID, contentID) {
|
|||
// We always schedule the flush in a timer even if it's very low or negative to allow
|
||||
// for multiple completeBoundary calls that are already queued to have a chance to
|
||||
// make the batch.
|
||||
setTimeout(revealCompletedBoundaries, msUntilTimeout);
|
||||
setTimeout(window['$RV'], msUntilTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ export {
|
|||
import escapeTextForBrowser from 'react-dom-bindings/src/server/escapeTextForBrowser';
|
||||
|
||||
export function getViewTransitionFormatContext(
|
||||
resumableState: ResumableState,
|
||||
parentContext: FormatContext,
|
||||
update: void | null | 'none' | 'auto' | string,
|
||||
enter: void | null | 'none' | 'auto' | string,
|
||||
|
|
|
|||
36
packages/react-server/src/ReactFizzServer.js
vendored
36
packages/react-server/src/ReactFizzServer.js
vendored
|
|
@ -1146,7 +1146,10 @@ function renderSuspenseBoundary(
|
|||
const prevKeyPath = someTask.keyPath;
|
||||
const prevContext = someTask.formatContext;
|
||||
someTask.keyPath = keyPath;
|
||||
someTask.formatContext = getSuspenseContentFormatContext(prevContext);
|
||||
someTask.formatContext = getSuspenseContentFormatContext(
|
||||
request.resumableState,
|
||||
prevContext,
|
||||
);
|
||||
const content: ReactNodeList = props.children;
|
||||
try {
|
||||
renderNode(request, someTask, content, -1);
|
||||
|
|
@ -1239,7 +1242,10 @@ function renderSuspenseBoundary(
|
|||
task.blockedSegment = boundarySegment;
|
||||
task.blockedPreamble = newBoundary.fallbackPreamble;
|
||||
task.keyPath = fallbackKeyPath;
|
||||
task.formatContext = getSuspenseFallbackFormatContext(prevContext);
|
||||
task.formatContext = getSuspenseFallbackFormatContext(
|
||||
request.resumableState,
|
||||
prevContext,
|
||||
);
|
||||
boundarySegment.status = RENDERING;
|
||||
try {
|
||||
renderNode(request, task, fallback, -1);
|
||||
|
|
@ -1278,7 +1284,10 @@ function renderSuspenseBoundary(
|
|||
newBoundary.contentState,
|
||||
task.abortSet,
|
||||
keyPath,
|
||||
getSuspenseContentFormatContext(task.formatContext),
|
||||
getSuspenseContentFormatContext(
|
||||
request.resumableState,
|
||||
task.formatContext,
|
||||
),
|
||||
task.context,
|
||||
task.treeContext,
|
||||
task.componentStack,
|
||||
|
|
@ -1305,7 +1314,10 @@ function renderSuspenseBoundary(
|
|||
task.hoistableState = newBoundary.contentState;
|
||||
task.blockedSegment = contentRootSegment;
|
||||
task.keyPath = keyPath;
|
||||
task.formatContext = getSuspenseContentFormatContext(prevContext);
|
||||
task.formatContext = getSuspenseContentFormatContext(
|
||||
request.resumableState,
|
||||
prevContext,
|
||||
);
|
||||
contentRootSegment.status = RENDERING;
|
||||
|
||||
try {
|
||||
|
|
@ -1409,7 +1421,10 @@ function renderSuspenseBoundary(
|
|||
newBoundary.fallbackState,
|
||||
fallbackAbortSet,
|
||||
fallbackKeyPath,
|
||||
getSuspenseFallbackFormatContext(task.formatContext),
|
||||
getSuspenseFallbackFormatContext(
|
||||
request.resumableState,
|
||||
task.formatContext,
|
||||
),
|
||||
task.context,
|
||||
task.treeContext,
|
||||
task.componentStack,
|
||||
|
|
@ -1471,7 +1486,10 @@ function replaySuspenseBoundary(
|
|||
task.blockedBoundary = resumedBoundary;
|
||||
task.hoistableState = resumedBoundary.contentState;
|
||||
task.keyPath = keyPath;
|
||||
task.formatContext = getSuspenseContentFormatContext(prevContext);
|
||||
task.formatContext = getSuspenseContentFormatContext(
|
||||
request.resumableState,
|
||||
prevContext,
|
||||
);
|
||||
task.replay = {nodes: childNodes, slots: childSlots, pendingTasks: 1};
|
||||
|
||||
try {
|
||||
|
|
@ -1569,7 +1587,10 @@ function replaySuspenseBoundary(
|
|||
resumedBoundary.fallbackState,
|
||||
fallbackAbortSet,
|
||||
fallbackKeyPath,
|
||||
getSuspenseFallbackFormatContext(task.formatContext),
|
||||
getSuspenseFallbackFormatContext(
|
||||
request.resumableState,
|
||||
task.formatContext,
|
||||
),
|
||||
task.context,
|
||||
task.treeContext,
|
||||
task.componentStack,
|
||||
|
|
@ -2284,6 +2305,7 @@ function renderViewTransition(
|
|||
request.resumableState,
|
||||
);
|
||||
task.formatContext = getViewTransitionFormatContext(
|
||||
request.resumableState,
|
||||
prevContext,
|
||||
getViewTransitionClassName(props.default, props.update),
|
||||
getViewTransitionClassName(props.default, props.enter),
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ const config = [
|
|||
entry: 'ReactDOMFizzInlineCompleteBoundary.js',
|
||||
exportName: 'completeBoundary',
|
||||
},
|
||||
{
|
||||
entry: 'ReactDOMFizzInlineCompleteBoundaryUpgradeToViewTransitions.js',
|
||||
exportName: 'completeBoundaryUpgradeToViewTransitions',
|
||||
},
|
||||
{
|
||||
entry: 'ReactDOMFizzInlineCompleteBoundaryWithStyles.js',
|
||||
exportName: 'completeBoundaryWithStyles',
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user