diff --git a/fixtures/view-transition/server/render.js b/fixtures/view-transition/server/render.js index 716290212e5a2..11d352eabdd72 100644 --- a/fixtures/view-transition/server/render.js +++ b/fixtures/view-transition/server/render.js @@ -23,8 +23,6 @@ export default function render(url, res) { const {pipe, abort} = renderToPipeableStream( , { - // 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. diff --git a/fixtures/view-transition/src/components/Page.js b/fixtures/view-transition/src/components/Page.js index 061b1edb2cbf6..39d0803af7700 100644 --- a/fixtures/view-transition/src/components/Page.js +++ b/fixtures/view-transition/src/components/Page.js @@ -200,21 +200,41 @@ export default function Page({url, navigate}) {
!!
- + +
+ +

█████

+
+

████

+

███████

+

████

+

██

+

██████

+

███

+

████

+
+ + }> -

these

-

rows

-

exist

-

to

-

test

-

scrolling

-

content

-

out

-

of

- {portal} -

the

-

viewport

- +
+

these

+

rows

+ +

exist

+
+

to

+

test

+

scrolling

+

content

+

out

+

of

+ {portal} +

the

+

viewport

+ +
{show ? : null} diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js index ef69bd4582a87..9418f6035b27d 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js @@ -759,10 +759,9 @@ const SUBTREE_SCOPE = ~(ENTER_SCOPE | EXIT_SCOPE); type ViewTransitionContext = { update: 'none' | 'auto' | string, - // null here means that this case can never trigger. Not "auto" like it does in props. - enter: null | 'none' | 'auto' | string, - exit: null | 'none' | 'auto' | string, - share: null | 'none' | 'auto' | string, + enter: 'none' | 'auto' | string, + exit: 'none' | 'auto' | string, + share: 'none' | 'auto' | string, name: 'auto' | string, autoName: string, // a name that can be used if an explicit one is not defined. nameIdx: number, // keeps track of how many duplicates of this name we've emitted. @@ -917,8 +916,8 @@ function getSuspenseViewTransition( // we would've used (the parent ViewTransition name or auto-assign one). const viewTransition: ViewTransitionContext = { update: parentViewTransition.update, // For deep updates. - enter: null, - exit: null, + enter: 'none', + exit: 'none', share: parentViewTransition.update, // For exit or enter of reveals. name: parentViewTransition.autoName, autoName: parentViewTransition.autoName, @@ -989,13 +988,8 @@ export function getViewTransitionFormatContext( share = parentViewTransition.share; } else { name = 'auto'; - share = null; // share is only relevant if there's an explicit name + share = 'none'; // share is only relevant if there's an explicit name } - } else if (share === 'none') { - // I believe if share is disabled, it means the same thing as if it doesn't - // 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'; @@ -1008,12 +1002,12 @@ export function getViewTransitionFormatContext( } } if (!(parentContext.tagScope & EXIT_SCOPE)) { - exit = null; // exit is only relevant for the first ViewTransition inside fallback + exit = 'none'; // 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 + enter = 'none'; // enter is only relevant for the first ViewTransition inside content } else { resumableState.instructions |= NeedUpgradeToViewTransitions; } @@ -1125,13 +1119,13 @@ function pushViewTransitionAttributes( viewTransition.nameIdx++; } pushStringAttribute(target, 'vt-update', viewTransition.update); - if (viewTransition.enter !== null) { + if (viewTransition.enter !== 'none') { pushStringAttribute(target, 'vt-enter', viewTransition.enter); } - if (viewTransition.exit !== null) { + if (viewTransition.exit !== 'none') { pushStringAttribute(target, 'vt-exit', viewTransition.exit); } - if (viewTransition.share !== null) { + if (viewTransition.share !== 'none') { pushStringAttribute(target, 'vt-share', viewTransition.share); } } diff --git a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js index aa7209fadac1d..363e11f988d5b 100644 --- a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js +++ b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetInlineCodeStrings.js @@ -6,9 +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=[];$RV=function(){$RT=performance.now();var d=$RB;$RB=[];for(var a=0;a
C + D ); @@ -2282,6 +2283,7 @@ describe('ReactDOMFizzStaticBrowser', () => { }); const prerendered = await pendingResult; + const postponedState = JSON.stringify(prerendered.postponed); await readIntoContainer(prerendered.prelude); @@ -2289,7 +2291,8 @@ describe('ReactDOMFizzStaticBrowser', () => {
{'Loading A'} {'Loading B'} - {'C' /* TODO: This should not be resolved. */} + {'Loading C'} + {'Loading D'}
, ); @@ -2309,6 +2312,7 @@ describe('ReactDOMFizzStaticBrowser', () => { {'A'} {'B'} {'C'} + {'D'} , ); }); diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index f3c70b66c064d..35c7909396edb 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -1725,6 +1725,26 @@ function unblockSuspenseListRow( } } +function trackPostponedSuspenseListRow( + request: Request, + trackedPostpones: PostponedHoles, + postponedRow: null | SuspenseListRow, +): void { + // TODO: Because we unconditionally call this, it will be called by finishedTask + // and so ends up recursive which can lead to stack overflow for very long lists. + if (postponedRow !== null) { + const postponedBoundaries = postponedRow.boundaries; + if (postponedBoundaries !== null) { + postponedRow.boundaries = null; + for (let i = 0; i < postponedBoundaries.length; i++) { + const postponedBoundary = postponedBoundaries[i]; + trackPostponedBoundary(request, trackedPostpones, postponedBoundary); + finishedTask(request, postponedBoundary, null, null); + } + } + } +} + function tryToResolveTogetherRow( request: Request, togetherRow: SuspenseListRow, @@ -3774,6 +3794,49 @@ function renderChildrenArray( } } +function trackPostponedBoundary( + request: Request, + trackedPostpones: PostponedHoles, + boundary: SuspenseBoundary, +): ReplaySuspenseBoundary { + boundary.status = POSTPONED; + // We need to eagerly assign it an ID because we'll need to refer to + // it before flushing and we know that we can't inline it. + boundary.rootSegmentID = request.nextSegmentId++; + + const boundaryKeyPath = boundary.trackedContentKeyPath; + if (boundaryKeyPath === null) { + throw new Error( + 'It should not be possible to postpone at the root. This is a bug in React.', + ); + } + + const fallbackReplayNode = boundary.trackedFallbackNode; + + const children: Array = []; + const boundaryNode: void | ReplayNode = + trackedPostpones.workingMap.get(boundaryKeyPath); + if (boundaryNode === undefined) { + const suspenseBoundary: ReplaySuspenseBoundary = [ + boundaryKeyPath[1], + boundaryKeyPath[2], + children, + null, + fallbackReplayNode, + boundary.rootSegmentID, + ]; + trackedPostpones.workingMap.set(boundaryKeyPath, suspenseBoundary); + addToReplayParent(suspenseBoundary, boundaryKeyPath[0], trackedPostpones); + return suspenseBoundary; + } else { + // Upgrade to ReplaySuspenseBoundary. + const suspenseBoundary: ReplaySuspenseBoundary = (boundaryNode: any); + suspenseBoundary[4] = fallbackReplayNode; + suspenseBoundary[5] = boundary.rootSegmentID; + return suspenseBoundary; + } +} + function trackPostpone( request: Request, trackedPostpones: PostponedHoles, @@ -3796,22 +3859,12 @@ function trackPostpone( } if (boundary !== null && boundary.status === PENDING) { - boundary.status = POSTPONED; - // We need to eagerly assign it an ID because we'll need to refer to - // it before flushing and we know that we can't inline it. - boundary.rootSegmentID = request.nextSegmentId++; - - const boundaryKeyPath = boundary.trackedContentKeyPath; - if (boundaryKeyPath === null) { - throw new Error( - 'It should not be possible to postpone at the root. This is a bug in React.', - ); - } - - const fallbackReplayNode = boundary.trackedFallbackNode; - - const children: Array = []; - if (boundaryKeyPath === keyPath && task.childIndex === -1) { + const boundaryNode = trackPostponedBoundary( + request, + trackedPostpones, + boundary, + ); + if (boundary.trackedContentKeyPath === keyPath && task.childIndex === -1) { // Assign ID if (segment.id === -1) { if (segment.parentFlushed) { @@ -3823,39 +3876,10 @@ function trackPostpone( } } // We postponed directly inside the Suspense boundary so we mark this for resuming. - const boundaryNode: ReplaySuspenseBoundary = [ - boundaryKeyPath[1], - boundaryKeyPath[2], - children, - segment.id, - fallbackReplayNode, - boundary.rootSegmentID, - ]; - trackedPostpones.workingMap.set(boundaryKeyPath, boundaryNode); - addToReplayParent(boundaryNode, boundaryKeyPath[0], trackedPostpones); + boundaryNode[3] = segment.id; return; - } else { - let boundaryNode: void | ReplayNode = - trackedPostpones.workingMap.get(boundaryKeyPath); - if (boundaryNode === undefined) { - boundaryNode = [ - boundaryKeyPath[1], - boundaryKeyPath[2], - children, - null, - fallbackReplayNode, - boundary.rootSegmentID, - ]; - trackedPostpones.workingMap.set(boundaryKeyPath, boundaryNode); - addToReplayParent(boundaryNode, boundaryKeyPath[0], trackedPostpones); - } else { - // Upgrade to ReplaySuspenseBoundary. - const suspenseBoundary: ReplaySuspenseBoundary = (boundaryNode: any); - suspenseBoundary[4] = fallbackReplayNode; - suspenseBoundary[5] = boundary.rootSegmentID; - } - // Fall through to add the child node. } + // Otherwise, fall through to add the child node. } // We know that this will leave a hole so we might as well assign an ID now. @@ -4941,7 +4965,18 @@ function finishedTask( } else if (boundary.status === POSTPONED) { const boundaryRow = boundary.row; if (boundaryRow !== null) { + if (request.trackedPostpones !== null) { + // If this boundary is postponed, then we need to also postpone any blocked boundaries + // in the next row. + trackPostponedSuspenseListRow( + request, + request.trackedPostpones, + boundaryRow.next, + ); + } if (--boundaryRow.pendingTasks === 0) { + // This is really unnecessary since we've already postponed the boundaries but + // for pairity with other track+finish paths. We might end up using the hoisting. finishSuspenseListRow(request, boundaryRow); } }