From 6060367ef8a7a5bac12e0f830367bb13626db83a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Sat, 17 May 2025 18:18:24 -0400 Subject: [PATCH 1/2] [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. --- fixtures/view-transition/server/render.js | 2 + .../view-transition/src/components/Page.js | 38 +++-- .../src/server/ReactFizzConfigDOM.js | 100 ++++++++--- .../src/server/ReactFizzConfigDOMLegacy.js | 1 + .../ReactDOMFizzInlineCompleteBoundary.js | 7 +- ...ompleteBoundaryUpgradeToViewTransitions.js | 10 ++ ...actDOMFizzInstructionSetExternalRuntime.js | 6 + ...tDOMFizzInstructionSetInlineCodeStrings.js | 4 +- .../ReactDOMFizzInstructionSetShared.js | 159 +++++++++++------- .../react-markup/src/ReactFizzConfigMarkup.js | 1 + packages/react-server/src/ReactFizzServer.js | 36 +++- .../rollup/generate-inline-fizz-runtime.js | 4 + 12 files changed, 259 insertions(+), 109 deletions(-) create mode 100644 packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteBoundaryUpgradeToViewTransitions.js diff --git a/fixtures/view-transition/server/render.js b/fixtures/view-transition/server/render.js index 11d352eabdd72..716290212e5a2 100644 --- a/fixtures/view-transition/server/render.js +++ b/fixtures/view-transition/server/render.js @@ -23,6 +23,8 @@ 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 db2cd0aff08c7..061b1edb2cbf6 100644 --- a/fixtures/view-transition/src/components/Page.js +++ b/fixtures/view-transition/src/components/Page.js @@ -8,6 +8,7 @@ import React, { useId, useOptimistic, startTransition, + Suspense, } from 'react'; import {createPortal} from 'react-dom'; @@ -60,6 +61,12 @@ function Id() { return ; } +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}) {
!!
-

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 8dbc6831e1fca..ef69bd4582a87 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js @@ -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) { diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js index 74787f9ee26b5..fd1791ff2c8a7 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js @@ -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, diff --git a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteBoundary.js b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteBoundary.js index 44a81c64468e7..761a8a18a958f 100644 --- a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteBoundary.js +++ b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteBoundary.js @@ -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; diff --git a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteBoundaryUpgradeToViewTransitions.js b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteBoundaryUpgradeToViewTransitions.js new file mode 100644 index 0000000000000..e033b9cc9e82b --- /dev/null +++ b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInlineCompleteBoundaryUpgradeToViewTransitions.js @@ -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'], +); diff --git a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetExternalRuntime.js b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetExternalRuntime.js index 845ea5b5666e3..ce503f815d168 100644 --- a/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetExternalRuntime.js +++ b/packages/react-dom-bindings/src/server/fizz-instruction-set/ReactDOMFizzInstructionSetExternalRuntime.js @@ -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; 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 470698b27c0c5..aa7209fadac1d 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,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 { + 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); } } diff --git a/packages/react-markup/src/ReactFizzConfigMarkup.js b/packages/react-markup/src/ReactFizzConfigMarkup.js index 3ed4e61190a00..bbca0d4ddf2b9 100644 --- a/packages/react-markup/src/ReactFizzConfigMarkup.js +++ b/packages/react-markup/src/ReactFizzConfigMarkup.js @@ -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, diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js index a159771421b7d..247e21076e5b6 100644 --- a/packages/react-server/src/ReactFizzServer.js +++ b/packages/react-server/src/ReactFizzServer.js @@ -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), diff --git a/scripts/rollup/generate-inline-fizz-runtime.js b/scripts/rollup/generate-inline-fizz-runtime.js index 3d097f63e216a..c531240e0c586 100644 --- a/scripts/rollup/generate-inline-fizz-runtime.js +++ b/scripts/rollup/generate-inline-fizz-runtime.js @@ -25,6 +25,10 @@ const config = [ entry: 'ReactDOMFizzInlineCompleteBoundary.js', exportName: 'completeBoundary', }, + { + entry: 'ReactDOMFizzInlineCompleteBoundaryUpgradeToViewTransitions.js', + exportName: 'completeBoundaryUpgradeToViewTransitions', + }, { entry: 'ReactDOMFizzInlineCompleteBoundaryWithStyles.js', exportName: 'completeBoundaryWithStyles', From 462d08f9ba41d48ab36bf405235c1c22023603dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Sat, 17 May 2025 20:00:56 -0400 Subject: [PATCH 2/2] Move SuspenseListProps into a shared/ReactTypes (#33298) So they can be shared by server. Incorporates the types from definitely typed too. --- .../src/ReactFiberBeginWork.js | 8 +++---- .../src/ReactFiberSuspenseComponent.js | 4 +--- packages/shared/ReactTypes.js | 24 +++++++++++++++++++ 3 files changed, 29 insertions(+), 7 deletions(-) diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index 2e76e5188ec5f..69bc84038dac9 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -14,6 +14,9 @@ import type { ViewTransitionProps, ActivityProps, SuspenseProps, + SuspenseListProps, + SuspenseListRevealOrder, + SuspenseListTailMode, TracingMarkerProps, CacheProps, ProfilerProps, @@ -26,7 +29,6 @@ import type {ActivityState} from './ReactFiberActivityComponent'; import type { SuspenseState, SuspenseListRenderState, - SuspenseListTailMode, } from './ReactFiberSuspenseComponent'; import type {SuspenseContext} from './ReactFiberSuspenseContext'; import type { @@ -3222,8 +3224,6 @@ function findLastContentRow(firstChild: null | Fiber): null | Fiber { return lastContentRow; } -type SuspenseListRevealOrder = 'forwards' | 'backwards' | 'together' | void; - function validateRevealOrder(revealOrder: SuspenseListRevealOrder) { if (__DEV__) { if ( @@ -3410,7 +3410,7 @@ function updateSuspenseListComponent( workInProgress: Fiber, renderLanes: Lanes, ) { - const nextProps = workInProgress.pendingProps; + const nextProps: SuspenseListProps = workInProgress.pendingProps; const revealOrder: SuspenseListRevealOrder = nextProps.revealOrder; const tailMode: SuspenseListTailMode = nextProps.tail; const newChildren = nextProps.children; diff --git a/packages/react-reconciler/src/ReactFiberSuspenseComponent.js b/packages/react-reconciler/src/ReactFiberSuspenseComponent.js index 07a7df2ec5b56..f7ae78ed6d313 100644 --- a/packages/react-reconciler/src/ReactFiberSuspenseComponent.js +++ b/packages/react-reconciler/src/ReactFiberSuspenseComponent.js @@ -7,7 +7,7 @@ * @flow */ -import type {Wakeable} from 'shared/ReactTypes'; +import type {Wakeable, SuspenseListTailMode} from 'shared/ReactTypes'; import type {Fiber} from './ReactInternalTypes'; import type {SuspenseInstance} from './ReactFiberConfig'; import type {Lane} from './ReactFiberLane'; @@ -42,8 +42,6 @@ export type SuspenseState = { hydrationErrors: Array> | null, }; -export type SuspenseListTailMode = 'collapsed' | 'hidden' | void; - export type SuspenseListRenderState = { isBackwards: boolean, // The currently rendering tail row. diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index 4c8ca77bb160b..9bd64cd81d608 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -290,6 +290,30 @@ export type SuspenseProps = { name?: string, }; +export type SuspenseListRevealOrder = + | 'forwards' + | 'backwards' + | 'together' + | void; + +export type SuspenseListTailMode = 'collapsed' | 'hidden' | void; + +type DirectionalSuspenseListProps = { + children?: ReactNodeList, + revealOrder: 'forwards' | 'backwards', + tail?: SuspenseListTailMode, +}; + +type NonDirectionalSuspenseListProps = { + children?: ReactNodeList, + revealOrder?: 'together' | void, + tail?: void, +}; + +export type SuspenseListProps = + | DirectionalSuspenseListProps + | NonDirectionalSuspenseListProps; + export type TracingMarkerProps = { name: string, children?: ReactNodeList,