@samitouri / QOS-React / commits / 8bda71558c

[Fiber] support hydration when rendering Suspense anywhere (#32224)

follow up to https://github.com/facebook/react/pull/32163 This continues the work of making Suspense workable anywhere in a react-dom tree. See the prior PRs for how we handle server rendering and client rendering. In this change we update the hydration implementation to be able to locate expected nodes. In particular this means hydration understands now that the default hydration context is the document body when the container is above the body. One case that is unique to hydration is clearing Suspense boundaries. When hydration fails or when the server instructs the client to recover an errored boundary it's possible that the html, head, and body tags in the initial document were written from a fallback or a different primary content on the server and need to be replaced by the client render. However these tags (and in the case of head, their content) won't be inside the comment nodes that identify the bounds of the Suspense boundary. And when client rendering you may not even render the same singletons that were server rendered. So when server rendering a boudnary which contributes to the preamble (the html, head, and body tag openings plus the head contents) we emit a special marker comment just before closing the boundary out. This marker encodes which parts of the preamble this boundary owned. If we need to clear the suspense boundary on the client we read this marker and use it to reset the appropriate singleton state.

Josh Story committed Feb 4, 2025 at 12:30 UTC 8bda71558c8b6f9f19af33271f1bfd0251a1c071
11 files changed +833 -40
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+108 -2
@@ -207,6 +207,9 @@ const SUSPENSE_START_DATA = '$';
207 const SUSPENSE_END_DATA = '/$';
208 const SUSPENSE_PENDING_START_DATA = '$?';
209 const SUSPENSE_FALLBACK_START_DATA = '$!';
210 +const PREAMBLE_CONTRIBUTION_HTML = 0b001;
211 +const PREAMBLE_CONTRIBUTION_BODY = 0b010;
212 +const PREAMBLE_CONTRIBUTION_HEAD = 0b100;
213 const FORM_STATE_IS_MATCHING = 'F!';
214 const FORM_STATE_IS_NOT_MATCHING = 'F';
215
@@ -963,6 +966,7 @@ export function clearSuspenseBoundary(
966 suspenseInstance: SuspenseInstance,
967 ): void {
968 let node: Node = suspenseInstance;
969 + let possiblePreambleContribution: number = 0;
970 // Delete all nodes within this suspense boundary.
971 // There might be nested nodes so we need to keep track of how
972 // deep we are and only break out when we're back on top.
@@ -973,6 +977,36 @@ export function clearSuspenseBoundary(
977 if (nextNode && nextNode.nodeType === COMMENT_NODE) {
978 const data = ((nextNode: any).data: string);
979 if (data === SUSPENSE_END_DATA) {
980 + if (
981 + // represents 3 bits where at least one bit is set (1-7)
982 + possiblePreambleContribution > 0 &&
983 + possiblePreambleContribution < 8
984 + ) {
985 + const code = possiblePreambleContribution;
986 + // It's not normally possible to insert a comment immediately preceding Suspense boundary
987 + // closing comment marker so we can infer that if the comment preceding starts with "1" through "7"
988 + // then it is in fact a preamble contribution marker comment. We do this value test to avoid the case
989 + // where the Suspense boundary is empty and the preceding comment marker is the Suspense boundary
990 + // opening marker or the closing marker of an inner boundary. In those cases the first character won't
991 + // have the requisite value to be interpreted as a Preamble contribution
992 + const ownerDocument = parentInstance.ownerDocument;
993 + if (code & PREAMBLE_CONTRIBUTION_HTML) {
994 + const documentElement: Element =
995 + (ownerDocument.documentElement: any);
996 + releaseSingletonInstance(documentElement);
997 + }
998 + if (code & PREAMBLE_CONTRIBUTION_BODY) {
999 + const body: Element = (ownerDocument.body: any);
1000 + releaseSingletonInstance(body);
1001 + }
1002 + if (code & PREAMBLE_CONTRIBUTION_HEAD) {
1003 + const head: Element = (ownerDocument.head: any);
1004 + releaseSingletonInstance(head);
1005 + // We need to clear the head because this is the only singleton that can have children that
1006 + // were part of this boundary but are not inside this boundary.
1007 + clearHead(head);
1008 + }
1009 + }
1010 if (depth === 0) {
1011 parentInstance.removeChild(nextNode);
1012 // Retry if any event replaying was blocked on this.
@@ -987,7 +1021,11 @@ export function clearSuspenseBoundary(
1021 data === SUSPENSE_FALLBACK_START_DATA
1022 ) {
1023 depth++;
1024 + } else {
1025 + possiblePreambleContribution = data.charCodeAt(0) - 48;
1026 }
1027 + } else {
1028 + possiblePreambleContribution = 0;
1029 }
1030 // $FlowFixMe[incompatible-type] we bail out when we get a null
1031 node = nextNode;
@@ -1501,7 +1539,7 @@ function clearContainerSparingly(container: Node) {
1539 case 'STYLE': {
1540 continue;
1541 }
1504 - // Stylesheet tags are retained because tehy may likely come from 3rd party scripts and extensions
1542 + // Stylesheet tags are retained because they may likely come from 3rd party scripts and extensions
1543 case 'LINK': {
1544 if (((node: any): HTMLLinkElement).rel.toLowerCase() === 'stylesheet') {
1545 continue;
@@ -1513,6 +1551,27 @@ function clearContainerSparingly(container: Node) {
1551 return;
1552 }
1553
1554 +function clearHead(head: Element): void {
1555 + let node = head.firstChild;
1556 + while (node) {
1557 + const nextNode = node.nextSibling;
1558 + const nodeName = node.nodeName;
1559 + if (
1560 + isMarkedHoistable(node) ||
1561 + nodeName === 'SCRIPT' ||
1562 + nodeName === 'STYLE' ||
1563 + (nodeName === 'LINK' &&
1564 + ((node: any): HTMLLinkElement).rel.toLowerCase() === 'stylesheet')
1565 + ) {
1566 + // retain these nodes
1567 + } else {
1568 + head.removeChild(node);
1569 + }
1570 + node = nextNode;
1571 + }
1572 + return;
1573 +}
1574 +
1575 // Making this so we can eventually move all of the instance caching to the commit phase.
1576 // Currently this is only used to associate fiber and props to instances for hydrating
1577 // HostSingletons. The reason we need it here is we only want to make this binding on commit
@@ -1874,7 +1933,20 @@ export function getFirstHydratableChild(
1933 export function getFirstHydratableChildWithinContainer(
1934 parentContainer: Container,
1935 ): null | HydratableInstance {
1877 - return getNextHydratable(parentContainer.firstChild);
1936 + let parentElement: Element;
1937 + switch (parentContainer.nodeType) {
1938 + case DOCUMENT_NODE:
1939 + parentElement = (parentContainer: any).body;
1940 + break;
1941 + default: {
1942 + if (parentContainer.nodeName === 'HTML') {
1943 + parentElement = (parentContainer: any).ownerDocument.body;
1944 + } else {
1945 + parentElement = (parentContainer: any);
1946 + }
1947 + }
1948 + }
1949 + return getNextHydratable(parentElement.firstChild);
1950 }
1951
1952 export function getFirstHydratableChildWithinSuspenseInstance(
@@ -1883,6 +1955,40 @@ export function getFirstHydratableChildWithinSuspenseInstance(
1955 return getNextHydratable(parentInstance.nextSibling);
1956 }
1957
1958 +// If it were possible to have more than one scope singleton in a DOM tree
1959 +// we would need to model this as a stack but since you can only have one <head>
1960 +// and head is the only singleton that is a scope in DOM we can get away with
1961 +// tracking this as a single value.
1962 +let previousHydratableOnEnteringScopedSingleton: null | HydratableInstance =
1963 + null;
1964 +
1965 +export function getFirstHydratableChildWithinSingleton(
1966 + type: string,
1967 + singletonInstance: Instance,
1968 + currentHydratableInstance: null | HydratableInstance,
1969 +): null | HydratableInstance {
1970 + if (isSingletonScope(type)) {
1971 + previousHydratableOnEnteringScopedSingleton = currentHydratableInstance;
1972 + return getNextHydratable(singletonInstance.firstChild);
1973 + } else {
1974 + return currentHydratableInstance;
1975 + }
1976 +}
1977 +
1978 +export function getNextHydratableSiblingAfterSingleton(
1979 + type: string,
1980 + currentHydratableInstance: null | HydratableInstance,
1981 +): null | HydratableInstance {
1982 + if (isSingletonScope(type)) {
1983 + const previousHydratableInstance =
1984 + previousHydratableOnEnteringScopedSingleton;
1985 + previousHydratableOnEnteringScopedSingleton = null;
1986 + return previousHydratableInstance;
1987 + } else {
1988 + return currentHydratableInstance;
1989 + }
1990 +}
1991 +
1992 export function describeHydratableInstanceForDevWarnings(
1993 instance: HydratableInstance,
1994 ): string | {type: string, props: $ReadOnly<Props>} {
packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
+78 -6
@@ -684,16 +684,23 @@ export function completeResumableState(resumableState: ResumableState): void {
684 resumableState.bootstrapModules = undefined;
685 }
686
687 +const NoContribution /* */ = 0b000;
688 +const HTMLContribution /* */ = 0b001;
689 +const BodyContribution /* */ = 0b010;
690 +const HeadContribution /* */ = 0b100;
691 +
692 export type PreambleState = {
693 htmlChunks: null | Array<Chunk | PrecomputedChunk>,
694 headChunks: null | Array<Chunk | PrecomputedChunk>,
695 bodyChunks: null | Array<Chunk | PrecomputedChunk>,
696 + contribution: number,
697 };
698 export function createPreambleState(): PreambleState {
699 return {
700 htmlChunks: null,
701 headChunks: null,
702 bodyChunks: null,
703 + contribution: NoContribution,
704 };
705 }
706
@@ -3227,7 +3234,7 @@ function pushStartHead(
3234 throw new Error(`The ${'`<head>`'} tag may only be rendered once.`);
3235 }
3236 preamble.headChunks = [];
3230 - return pushStartGenericElement(preamble.headChunks, props, 'head');
3237 + return pushStartSingletonElement(preamble.headChunks, props, 'head');
3238 } else {
3239 // This <head> is deep and is likely just an error. we emit it inline though.
3240 // Validation should warn that this tag is the the wrong spot.
@@ -3251,7 +3258,7 @@ function pushStartBody(
3258 }
3259
3260 preamble.bodyChunks = [];
3254 - return pushStartGenericElement(preamble.bodyChunks, props, 'body');
3261 + return pushStartSingletonElement(preamble.bodyChunks, props, 'body');
3262 } else {
3263 // This <head> is deep and is likely just an error. we emit it inline though.
3264 // Validation should warn that this tag is the the wrong spot.
@@ -3275,7 +3282,7 @@ function pushStartHtml(
3282 }
3283
3284 preamble.htmlChunks = [DOCTYPE];
3278 - return pushStartGenericElement(preamble.htmlChunks, props, 'html');
3285 + return pushStartSingletonElement(preamble.htmlChunks, props, 'html');
3286 } else {
3287 // This <html> is deep and is likely just an error. we emit it inline though.
3288 // Validation should warn that this tag is the the wrong spot.
@@ -3416,6 +3423,43 @@ function pushScriptImpl(
3423 return null;
3424 }
3425
3426 +// This is a fork of pushStartGenericElement because we don't ever want to do
3427 +// the children as strign optimization on that path when rendering singletons.
3428 +// When we eliminate that special path we can delete this fork and unify it again
3429 +function pushStartSingletonElement(
3430 + target: Array<Chunk | PrecomputedChunk>,
3431 + props: Object,
3432 + tag: string,
3433 +): ReactNodeList {
3434 + target.push(startChunkForTag(tag));
3435 +
3436 + let children = null;
3437 + let innerHTML = null;
3438 + for (const propKey in props) {
3439 + if (hasOwnProperty.call(props, propKey)) {
3440 + const propValue = props[propKey];
3441 + if (propValue == null) {
3442 + continue;
3443 + }
3444 + switch (propKey) {
3445 + case 'children':
3446 + children = propValue;
3447 + break;
3448 + case 'dangerouslySetInnerHTML':
3449 + innerHTML = propValue;
3450 + break;
3451 + default:
3452 + pushAttribute(target, propKey, propValue);
3453 + break;
3454 + }
3455 + }
3456 + }
3457 +
3458 + target.push(endOfStartTag);
3459 + pushInnerHTML(target, innerHTML, children);
3460 + return children;
3461 +}
3462 +
3463 function pushStartGenericElement(
3464 target: Array<Chunk | PrecomputedChunk>,
3465 props: Object,
@@ -3907,14 +3951,17 @@ export function hoistPreambleState(
3951 preambleState: PreambleState,
3952 ) {
3953 const rootPreamble = renderState.preamble;
3910 - if (rootPreamble.htmlChunks === null) {
3954 + if (rootPreamble.htmlChunks === null && preambleState.htmlChunks) {
3955 rootPreamble.htmlChunks = preambleState.htmlChunks;
3956 + preambleState.contribution |= HTMLContribution;
3957 }
3913 - if (rootPreamble.headChunks === null) {
3958 + if (rootPreamble.headChunks === null && preambleState.headChunks) {
3959 rootPreamble.headChunks = preambleState.headChunks;
3960 + preambleState.contribution |= HeadContribution;
3961 }
3916 - if (rootPreamble.bodyChunks === null) {
3962 + if (rootPreamble.bodyChunks === null && preambleState.bodyChunks) {
3963 rootPreamble.bodyChunks = preambleState.bodyChunks;
3964 + preambleState.contribution |= BodyContribution;
3965 }
3966 }
3967
@@ -4091,7 +4138,11 @@ export function writeStartClientRenderedSuspenseBoundary(
4138 export function writeEndCompletedSuspenseBoundary(
4139 destination: Destination,
4140 renderState: RenderState,
4141 + preambleState: null | PreambleState,
4142 ): boolean {
4143 + if (preambleState) {
4144 + writePreambleContribution(destination, preambleState);
4145 + }
4146 return writeChunkAndReturn(destination, endSuspenseBoundary);
4147 }
4148 export function writeEndPendingSuspenseBoundary(
@@ -4103,10 +4154,31 @@ export function writeEndPendingSuspenseBoundary(
4154 export function writeEndClientRenderedSuspenseBoundary(
4155 destination: Destination,
4156 renderState: RenderState,
4157 + preambleState: null | PreambleState,
4158 ): boolean {
4159 + if (preambleState) {
4160 + writePreambleContribution(destination, preambleState);
4161 + }
4162 return writeChunkAndReturn(destination, endSuspenseBoundary);
4163 }
4164
4165 +const boundaryPreambleContributionChunkStart = stringToPrecomputedChunk('<!--');
4166 +const boundaryPreambleContributionChunkEnd = stringToPrecomputedChunk('-->');
4167 +
4168 +function writePreambleContribution(
4169 + destination: Destination,
4170 + preambleState: PreambleState,
4171 +) {
4172 + const contribution = preambleState.contribution;
4173 + if (contribution !== NoContribution) {
4174 + writeChunk(destination, boundaryPreambleContributionChunkStart);
4175 + // This is a number type so we can do the fast path without coercion checking
4176 + // eslint-disable-next-line react-internal/safe-string-coercion
4177 + writeChunk(destination, stringToChunk('' + contribution));
4178 + writeChunk(destination, boundaryPreambleContributionChunkEnd);
4179 + }
4180 +}
4181 +
4182 const startSegmentHTML = stringToPrecomputedChunk('<div hidden id="');
4183 const startSegmentHTML2 = stringToPrecomputedChunk('">');
4184 const endSegmentHTML = stringToPrecomputedChunk('</div>');
packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js
+12 -2
@@ -244,20 +244,30 @@ export function writeStartClientRenderedSuspenseBoundary(
244 export function writeEndCompletedSuspenseBoundary(
245 destination: Destination,
246 renderState: RenderState,
247 + preambleState: null | PreambleState,
248 ): boolean {
249 if (renderState.generateStaticMarkup) {
250 return true;
251 }
251 - return writeEndCompletedSuspenseBoundaryImpl(destination, renderState);
252 + return writeEndCompletedSuspenseBoundaryImpl(
253 + destination,
254 + renderState,
255 + preambleState,
256 + );
257 }
258 export function writeEndClientRenderedSuspenseBoundary(
259 destination: Destination,
260 renderState: RenderState,
261 + preambleState: null | PreambleState,
262 ): boolean {
263 if (renderState.generateStaticMarkup) {
264 return true;
265 }
260 - return writeEndClientRenderedSuspenseBoundaryImpl(destination, renderState);
266 + return writeEndClientRenderedSuspenseBoundaryImpl(
267 + destination,
268 + renderState,
269 + preambleState,
270 + );
271 }
272
273 export type TransitionStatus = FormStatus;
packages/react-dom/src/__tests__/ReactDOMFizzServer-test.js
+580 -8
@@ -8923,7 +8923,7 @@ describe('ReactDOMFizzServer', () => {
8923 );
8924 });
8925
8926 - it('can server render Suspense before, after, and around <html>', async () => {
8926 + it('can render Suspense before, after, and around <html>', async () => {
8927 function BlockedOn({value, children}) {
8928 readText(value);
8929 return children;
@@ -8989,9 +8989,33 @@ describe('ReactDOMFizzServer', () => {
8989 </body>
8990 </html>,
8991 );
8992 +
8993 + const root = ReactDOMClient.hydrateRoot(document, <App />);
8994 + await waitForAll([]);
8995 + expect(getVisibleChildren(document)).toEqual(
8996 + <html lang="en">
8997 + <head>
8998 + <meta itemprop="" content="non-floaty meta" />
8999 + </head>
9000 + <body>
9001 + <div>before</div>
9002 + <div>hello world</div>
9003 + <div>after</div>
9004 + </body>
9005 + </html>,
9006 + );
9007 + assertConsoleErrorDev(['In HTML, <div> cannot be a child of <#document>']);
9008 +
9009 + root.unmount();
9010 + expect(getVisibleChildren(document)).toEqual(
9011 + <html>
9012 + <head />
9013 + <body />
9014 + </html>,
9015 + );
9016 });
9017
8994 - it('can server render Suspense before, after, and around <body>', async () => {
9018 + it('can render Suspense before, after, and around <body>', async () => {
9019 function BlockedOn({value, children}) {
9020 readText(value);
9021 return children;
@@ -9052,9 +9076,83 @@ describe('ReactDOMFizzServer', () => {
9076 </body>
9077 </html>,
9078 );
9079 +
9080 + const root = ReactDOMClient.hydrateRoot(document, <App />);
9081 + await waitForAll([]);
9082 + expect(getVisibleChildren(document)).toEqual(
9083 + <html>
9084 + <head>
9085 + <meta content="before" />
9086 + <meta content="after" />
9087 + </head>
9088 + <body lang="en">
9089 + <meta itemprop="" content="before" />
9090 + <div>hello world</div>
9091 + <meta itemprop="" content="after" />
9092 + </body>
9093 + </html>,
9094 + );
9095 + if (gate(flags => flags.enableOwnerStacks)) {
9096 + assertConsoleErrorDev([
9097 + [
9098 + 'Cannot render a <meta> outside the main document if it has an `itemProp` prop. `itemProp` suggests the tag belongs to an `itemScope` which can appear anywhere in the DOM. If you were intending for React to hoist this <meta> remove the `itemProp` prop. Otherwise, try moving this tag into the <head> or <body> of the Document.',
9099 + {withoutStack: true},
9100 + ],
9101 + 'In HTML, <meta> cannot be a child of <html>.\nThis will cause a hydration error.' +
9102 + '\n' +
9103 + '\n <App>' +
9104 + '\n> <html>' +
9105 + '\n <Suspense fallback="this fallb...">' +
9106 + '\n <meta>' +
9107 + '\n> <meta itemProp="" content="before">' +
9108 + '\n ...' +
9109 + '\n' +
9110 + '\n in meta (at **)' +
9111 + '\n in App (at **)',
9112 + '<html> cannot contain a nested <meta>.\nSee this log for the ancestor stack trace.' +
9113 + '\n in html (at **)' +
9114 + '\n in App (at **)',
9115 + [
9116 + 'Cannot render a <meta> outside the main document if it has an `itemProp` prop. `itemProp` suggests the tag belongs to an `itemScope` which can appear anywhere in the DOM. If you were intending for React to hoist this <meta> remove the `itemProp` prop. Otherwise, try moving this tag into the <head> or <body> of the Document.',
9117 + {withoutStack: true},
9118 + ],
9119 + ]);
9120 + } else {
9121 + assertConsoleErrorDev([
9122 + 'Cannot render a <meta> outside the main document if it has an `itemProp` prop. `itemProp` suggests the tag belongs to an `itemScope` which can appear anywhere in the DOM. If you were intending for React to hoist this <meta> remove the `itemProp` prop. Otherwise, try moving this tag into the <head> or <body> of the Document.' +
9123 + '\n in Suspense (at **)' +
9124 + '\n in html (at **)' +
9125 + '\n in App (at **)',
9126 + 'In HTML, <meta> cannot be a child of <html>.\nThis will cause a hydration error.' +
9127 + '\n' +
9128 + '\n <App>' +
9129 + '\n> <html>' +
9130 + '\n <Suspense fallback="this fallb...">' +
9131 + '\n <meta>' +
9132 + '\n> <meta itemProp="" content="before">' +
9133 + '\n ...' +
9134 + '\n' +
9135 + '\n in meta (at **)' +
9136 + '\n in Suspense (at **)' +
9137 + '\n in html (at **)' +
9138 + '\n in App (at **)',
9139 + 'Cannot render a <meta> outside the main document if it has an `itemProp` prop. `itemProp` suggests the tag belongs to an `itemScope` which can appear anywhere in the DOM. If you were intending for React to hoist this <meta> remove the `itemProp` prop. Otherwise, try moving this tag into the <head> or <body> of the Document.' +
9140 + '\n in Suspense (at **)' +
9141 + '\n in html (at **)' +
9142 + '\n in App (at **)',
9143 + ]);
9144 + }
9145 +
9146 + await root.unmount();
9147 + expect(getVisibleChildren(document)).toEqual(
9148 + <html>
9149 + <head />
9150 + <body />
9151 + </html>,
9152 + );
9153 });
9154
9057 - it('can server render Suspense before, after, and around <head>', async () => {
9155 + it('can render Suspense before, after, and around <head>', async () => {
9156 function BlockedOn({value, children}) {
9157 readText(value);
9158 return children;
@@ -9119,11 +9217,90 @@ describe('ReactDOMFizzServer', () => {
9217 </body>
9218 </html>,
9219 );
9220 +
9221 + const root = ReactDOMClient.hydrateRoot(document, <App />);
9222 + await waitForAll([]);
9223 + expect(getVisibleChildren(document)).toEqual(
9224 + <html>
9225 + <head lang="en">
9226 + <meta content="before" />
9227 + <meta content="after" />
9228 + <meta itemprop="" />
9229 + </head>
9230 + <body>
9231 + <meta itemprop="" content="before" />
9232 + <meta itemprop="" content="after" />
9233 + <div>hello world</div>
9234 + </body>
9235 + </html>,
9236 + );
9237 + if (gate(flags => flags.enableOwnerStacks)) {
9238 + assertConsoleErrorDev([
9239 + [
9240 + 'Cannot render a <meta> outside the main document if it has an `itemProp` prop. `itemProp` suggests the tag belongs to an `itemScope` which can appear anywhere in the DOM. If you were intending for React to hoist this <meta> remove the `itemProp` prop. Otherwise, try moving this tag into the <head> or <body> of the Document.',
9241 + {withoutStack: true},
9242 + ],
9243 + 'In HTML, <meta> cannot be a child of <html>.\nThis will cause a hydration error.' +
9244 + '\n' +
9245 + '\n <App>' +
9246 + '\n> <html>' +
9247 + '\n <Suspense fallback="this fallb...">' +
9248 + '\n <meta>' +
9249 + '\n> <meta itemProp="" content="before">' +
9250 + '\n ...' +
9251 + '\n' +
9252 + '\n in meta (at **)' +
9253 + '\n in App (at **)',
9254 + '<html> cannot contain a nested <meta>.\nSee this log for the ancestor stack trace.' +
9255 + '\n in html (at **)' +
9256 + '\n in App (at **)',
9257 + [
9258 + 'Cannot render a <meta> outside the main document if it has an `itemProp` prop. `itemProp` suggests the tag belongs to an `itemScope` which can appear anywhere in the DOM. If you were intending for React to hoist this <meta> remove the `itemProp` prop. Otherwise, try moving this tag into the <head> or <body> of the Document.',
9259 + {withoutStack: true},
9260 + ],
9261 + ]);
9262 + } else {
9263 + assertConsoleErrorDev([
9264 + 'Cannot render a <meta> outside the main document if it has an `itemProp` prop. `itemProp` suggests the tag belongs to an `itemScope` which can appear anywhere in the DOM. If you were intending for React to hoist this <meta> remove the `itemProp` prop. Otherwise, try moving this tag into the <head> or <body> of the Document.' +
9265 + '\n in Suspense (at **)' +
9266 + '\n in html (at **)' +
9267 + '\n in App (at **)',
9268 + 'In HTML, <meta> cannot be a child of <html>.\nThis will cause a hydration error.' +
9269 + '\n' +
9270 + '\n <App>' +
9271 + '\n> <html>' +
9272 + '\n <Suspense fallback="this fallb...">' +
9273 + '\n <meta>' +
9274 + '\n> <meta itemProp="" content="before">' +
9275 + '\n ...' +
9276 + '\n' +
9277 + '\n in meta (at **)' +
9278 + '\n in Suspense (at **)' +
9279 + '\n in html (at **)' +
9280 + '\n in App (at **)',
9281 + 'Cannot render a <meta> outside the main document if it has an `itemProp` prop. `itemProp` suggests the tag belongs to an `itemScope` which can appear anywhere in the DOM. If you were intending for React to hoist this <meta> remove the `itemProp` prop. Otherwise, try moving this tag into the <head> or <body> of the Document.' +
9282 + '\n in Suspense (at **)' +
9283 + '\n in html (at **)' +
9284 + '\n in App (at **)',
9285 + ]);
9286 + }
9287 +
9288 + await root.unmount();
9289 + expect(getVisibleChildren(document)).toEqual(
9290 + <html>
9291 + <head />
9292 + <body />
9293 + </html>,
9294 + );
9295 });
9296
9124 - it('will render fallback Document when erroring a boundary above the body', async () => {
9297 + it('will render fallback Document when erroring a boundary above the body and recover on the client', async () => {
9298 + let serverRendering = true;
9299 function Boom() {
9126 - throw new Error('Boom!');
9300 + if (serverRendering) {
9301 + throw new Error('Boom!');
9302 + }
9303 + return null;
9304 }
9305
9306 function App() {
@@ -9174,11 +9351,50 @@ describe('ReactDOMFizzServer', () => {
9351 </body>
9352 </html>,
9353 );
9354 +
9355 + serverRendering = false;
9356 +
9357 + const recoverableErrors = [];
9358 + const root = ReactDOMClient.hydrateRoot(document, <App />, {
9359 + onRecoverableError(err) {
9360 + recoverableErrors.push(err);
9361 + },
9362 + });
9363 + await waitForAll([]);
9364 + expect(getVisibleChildren(document)).toEqual(
9365 + <html data-content-html="">
9366 + <head />
9367 + <body data-content-body="">
9368 + <span>hello world</span>
9369 + </body>
9370 + </html>,
9371 + );
9372 + expect(recoverableErrors).toEqual([
9373 + __DEV__
9374 + ? new Error(
9375 + 'Switched to client rendering because the server rendering errored:\n\nBoom!',
9376 + )
9377 + : new Error(
9378 + 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
9379 + ),
9380 + ]);
9381 +
9382 + root.unmount();
9383 + expect(getVisibleChildren(document)).toEqual(
9384 + <html>
9385 + <head />
9386 + <body />
9387 + </html>,
9388 + );
9389 });
9390
9391 it('will hoist resources and hositables from a primary tree into the <head> of a client rendered fallback', async () => {
9392 + let serverRendering = true;
9393 function Boom() {
9181 - throw new Error('Boom!');
9394 + if (serverRendering) {
9395 + throw new Error('Boom!');
9396 + }
9397 + return null;
9398 }
9399
9400 function App() {
@@ -9255,6 +9471,65 @@ describe('ReactDOMFizzServer', () => {
9471 </body>
9472 </html>,
9473 );
9474 +
9475 + serverRendering = false;
9476 +
9477 + const recoverableErrors = [];
9478 + const root = ReactDOMClient.hydrateRoot(document, <App />, {
9479 + onRecoverableError(err) {
9480 + recoverableErrors.push(err);
9481 + },
9482 + });
9483 + await waitForAll([]);
9484 + expect(recoverableErrors).toEqual([
9485 + __DEV__
9486 + ? new Error(
9487 + 'Switched to client rendering because the server rendering errored:\n\nBoom!',
9488 + )
9489 + : new Error(
9490 + 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
9491 + ),
9492 + ]);
9493 + expect(getVisibleChildren(document)).toEqual(
9494 + <html data-content-html="">
9495 + <head>
9496 + <link
9497 + rel="stylesheet"
9498 + href="hoistable before"
9499 + data-precedence="default"
9500 + />
9501 + <link
9502 + rel="stylesheet"
9503 + href="hoistable after"
9504 + data-precedence="default"
9505 + />
9506 + <meta content="hoistable before" />
9507 + <meta content="hoistable after" />
9508 + </head>
9509 + <body data-content-body="">
9510 + <span>hello world</span>
9511 + </body>
9512 + </html>,
9513 + );
9514 +
9515 + root.unmount();
9516 + expect(getVisibleChildren(document)).toEqual(
9517 + <html>
9518 + <head>
9519 + <link
9520 + rel="stylesheet"
9521 + href="hoistable before"
9522 + data-precedence="default"
9523 + />
9524 + <link
9525 + rel="stylesheet"
9526 + href="hoistable after"
9527 + data-precedence="default"
9528 + />
9529 + </head>
9530 + <body />
9531 + </html>,
9532 + );
9533 });
9534
9535 it('Will wait to flush Document chunks until all boundaries which might contain a preamble are errored or resolved', async () => {
@@ -9353,8 +9628,12 @@ describe('ReactDOMFizzServer', () => {
9628 });
9629
9630 it('Can render a fallback <head> alongside a non-fallback body', async () => {
9631 + let serverRendering = true;
9632 function Boom() {
9357 - throw new Error('Boom!');
9633 + if (serverRendering) {
9634 + throw new Error('Boom!');
9635 + }
9636 + return null;
9637 }
9638
9639 function App() {
@@ -9416,11 +9695,52 @@ describe('ReactDOMFizzServer', () => {
9695 </body>
9696 </html>,
9697 );
9698 +
9699 + serverRendering = false;
9700 +
9701 + const recoverableErrors = [];
9702 + const root = ReactDOMClient.hydrateRoot(document, <App />, {
9703 + onRecoverableError(err) {
9704 + recoverableErrors.push(err);
9705 + },
9706 + });
9707 + await waitForAll([]);
9708 + expect(recoverableErrors).toEqual([
9709 + __DEV__
9710 + ? new Error(
9711 + 'Switched to client rendering because the server rendering errored:\n\nBoom!',
9712 + )
9713 + : new Error(
9714 + 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
9715 + ),
9716 + ]);
9717 + expect(getVisibleChildren(document)).toEqual(
9718 + <html>
9719 + <head data-primary="">
9720 + <meta itemprop="" content="primary" />
9721 + </head>
9722 + <body data-primary="">
9723 + <div>primary body</div>
9724 + </body>
9725 + </html>,
9726 + );
9727 +
9728 + root.unmount();
9729 + expect(getVisibleChildren(document)).toEqual(
9730 + <html>
9731 + <head />
9732 + <body />
9733 + </html>,
9734 + );
9735 });
9736
9737 it('Can render a fallback <body> alongside a non-fallback head', async () => {
9738 + let serverRendering = true;
9739 function Boom() {
9423 - throw new Error('Boom!');
9740 + if (serverRendering) {
9741 + throw new Error('Boom!');
9742 + }
9743 + return null;
9744 }
9745
9746 function App() {
@@ -9482,6 +9802,43 @@ describe('ReactDOMFizzServer', () => {
9802 </body>
9803 </html>,
9804 );
9805 +
9806 + serverRendering = false;
9807 +
9808 + const recoverableErrors = [];
9809 + const root = ReactDOMClient.hydrateRoot(document, <App />, {
9810 + onRecoverableError(err) {
9811 + recoverableErrors.push(err);
9812 + },
9813 + });
9814 + await waitForAll([]);
9815 + expect(getVisibleChildren(document)).toEqual(
9816 + <html>
9817 + <head data-primary="">
9818 + <meta itemprop="" content="primary" />
9819 + </head>
9820 + <body data-primary="">
9821 + <div>primary body</div>
9822 + </body>
9823 + </html>,
9824 + );
9825 + expect(recoverableErrors).toEqual([
9826 + __DEV__
9827 + ? new Error(
9828 + 'Switched to client rendering because the server rendering errored:\n\nBoom!',
9829 + )
9830 + : new Error(
9831 + 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
9832 + ),
9833 + ]);
9834 +
9835 + root.unmount();
9836 + expect(getVisibleChildren(document)).toEqual(
9837 + <html>
9838 + <head />
9839 + <body />
9840 + </html>,
9841 + );
9842 });
9843
9844 it('Can render a <head> outside of a containing <html>', async () => {
@@ -9528,6 +9885,27 @@ describe('ReactDOMFizzServer', () => {
9885 </body>
9886 </html>,
9887 );
9888 +
9889 + const root = ReactDOMClient.hydrateRoot(document, <App />);
9890 + await waitForAll([]);
9891 + expect(getVisibleChildren(document)).toEqual(
9892 + <html data-x="">
9893 + <head data-y="">
9894 + <meta itemprop="" />
9895 + </head>
9896 + <body data-x="">
9897 + <span>hello world</span>
9898 + </body>
9899 + </html>,
9900 + );
9901 +
9902 + root.unmount();
9903 + expect(getVisibleChildren(document)).toEqual(
9904 + <html>
9905 + <head />
9906 + <body />
9907 + </html>,
9908 + );
9909 });
9910
9911 it('can render preamble tags in deeply nested indirect component trees', async () => {
@@ -9661,6 +10039,28 @@ describe('ReactDOMFizzServer', () => {
10039 </body>
10040 </html>,
10041 );
10042 +
10043 + const root = ReactDOMClient.hydrateRoot(document, <App />);
10044 + await waitForAll([]);
10045 + expect(getVisibleChildren(document)).toEqual(
10046 + <html lang="es">
10047 + <head data-main="">
10048 + <meta content="author" />
10049 + <meta content="published date" />
10050 + </head>
10051 + <body data-main="">
10052 + <div>This is soooo cool!</div>
10053 + </body>
10054 + </html>,
10055 + );
10056 +
10057 + root.unmount();
10058 + expect(getVisibleChildren(document)).toEqual(
10059 + <html>
10060 + <head />
10061 + <body />
10062 + </html>,
10063 + );
10064 });
10065
10066 it('will flush the preamble as soon as a complete preamble is available', async () => {
@@ -9740,5 +10140,177 @@ describe('ReactDOMFizzServer', () => {
10140 </body>
10141 </html>,
10142 );
10143 +
10144 + const root = ReactDOMClient.hydrateRoot(document, <App />);
10145 + await waitForAll([]);
10146 + expect(getVisibleChildren(document)).toEqual(
10147 + <html>
10148 + <head>
10149 + <meta content="head" />
10150 + </head>
10151 + <body>
10152 + loading before...
10153 + <div>body</div>
10154 + loading after...
10155 + </body>
10156 + </html>,
10157 + );
10158 +
10159 + await act(() => {
10160 + resolveText('before');
10161 + resolveText('after');
10162 + });
10163 + await waitForAll([]);
10164 + expect(getVisibleChildren(document)).toEqual(
10165 + <html>
10166 + <head>
10167 + <meta content="head" />
10168 + </head>
10169 + <body>
10170 + <div>before</div>
10171 + <div>body</div>
10172 + <div>after</div>
10173 + </body>
10174 + </html>,
10175 + );
10176 + assertConsoleErrorDev(['In HTML, <div> cannot be a child of <#document>']);
10177 +
10178 + root.unmount();
10179 + expect(getVisibleChildren(document)).toEqual(
10180 + <html>
10181 + <head />
10182 + <body />
10183 + </html>,
10184 + );
10185 + });
10186 +
10187 + it('will clean up the head when a hydration mismatch causes a boundary to recover on the client', async () => {
10188 + let content = 'server';
10189 +
10190 + function ServerApp() {
10191 + return (
10192 + <Suspense>
10193 + <html data-x={content}>
10194 + <head data-x={content}>
10195 + <meta itemProp="" content={content} />
10196 + </head>
10197 + <body data-x={content}>{content}</body>
10198 + </html>
10199 + </Suspense>
10200 + );
10201 + }
10202 +
10203 + function ClientApp() {
10204 + return (
10205 + <Suspense>
10206 + <html data-y={content}>
10207 + <head data-y={content}>
10208 + <meta itemProp="" name={content} />
10209 + </head>
10210 + <body data-y={content}>{content}</body>
10211 + </html>
10212 + </Suspense>
10213 + );
10214 + }
10215 +
10216 + await act(() => {
10217 + const {pipe} = renderToPipeableStream(<ServerApp />);
10218 + pipe(writable);
10219 + });
10220 + expect(getVisibleChildren(document)).toEqual(
10221 + <html data-x="server">
10222 + <head data-x="server">
10223 + <meta itemprop="" content="server" />
10224 + </head>
10225 + <body data-x="server">server</body>
10226 + </html>,
10227 + );
10228 +
10229 + content = 'client';
10230 +
10231 + const recoverableErrors = [];
10232 + const root = ReactDOMClient.hydrateRoot(document, <ClientApp />, {
10233 + onRecoverableError(err) {
10234 + recoverableErrors.push(err.message);
10235 + },
10236 + });
10237 + await waitForAll([]);
10238 + if (gate(flags => flags.favorSafetyOverHydrationPerf)) {
10239 + expect(getVisibleChildren(document)).toEqual(
10240 + <html data-y="client">
10241 + <head data-y="client">
10242 + <meta itemprop="" name="client" />
10243 + </head>
10244 + <body data-y="client">client</body>
10245 + </html>,
10246 + );
10247 + expect(recoverableErrors).toEqual([
10248 + expect.stringContaining(
10249 + "Hydration failed because the server rendered HTML didn't match the client.",
10250 + ),
10251 + ]);
10252 + } else {
10253 + expect(getVisibleChildren(document)).toEqual(
10254 + <html data-x="server">
10255 + <head data-x="server">
10256 + <meta itemprop="" content="server" />
10257 + </head>
10258 + <body data-x="server">server</body>
10259 + </html>,
10260 + );
10261 + expect(recoverableErrors).toEqual([]);
10262 + assertConsoleErrorDev([
10263 + "A tree hydrated but some attributes of the server rendered HTML didn't match the client properties. This won't be patched up. This can happen if a SSR-ed Client Component used:" +
10264 + '\n' +
10265 + "\n- A server/client branch `if (typeof window !== 'undefined')`." +
10266 + "\n- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called." +
10267 + "\n- Date formatting in a user's locale which doesn't match the server." +
10268 + '\n- External changing data without sending a snapshot of it along with the HTML.' +
10269 + '\n- Invalid HTML tag nesting.' +
10270 + '\n' +
10271 + '\nIt can also happen if the client has a browser extension installed which messes with the HTML before React loaded.' +
10272 + '\n' +
10273 + '\nhttps://react.dev/link/hydration-mismatch' +
10274 + '\n' +
10275 + '\n <ClientApp>' +
10276 + '\n <Suspense>' +
10277 + '\n <html' +
10278 + '\n+ data-y="client"' +
10279 + '\n- data-y={null}' +
10280 + '\n- data-x="server"' +
10281 + '\n >' +
10282 + '\n <head' +
10283 + '\n+ data-y="client"' +
10284 + '\n- data-y={null}' +
10285 + '\n- data-x="server"' +
10286 + '\n >' +
10287 + '\n <meta' +
10288 + '\n itemProp=""' +
10289 + '\n+ name="client"' +
10290 + '\n- name={null}' +
10291 + '\n- content="server"' +
10292 + '\n >' +
10293 + '\n <body' +
10294 + '\n+ data-y="client"' +
10295 + '\n- data-y={null}' +
10296 + '\n- data-x="server"' +
10297 + '\n >' +
10298 + '\n+ client' +
10299 + '\n- server' +
10300 + '\n+ client' +
10301 + '\n- server' +
10302 + '\n' +
10303 + '\n in Suspense (at **)' +
10304 + '\n in ClientApp (at **)',
10305 + ]);
10306 + }
10307 +
10308 + root.unmount();
10309 + expect(getVisibleChildren(document)).toEqual(
10310 + <html>
10311 + <head />
10312 + <body />
10313 + </html>,
10314 + );
10315 });
10316 });
packages/react-dom/src/__tests__/ReactDOMServerIntegrationUntrustedURL-test.js
+6
@@ -41,6 +41,7 @@ describe('ReactDOMServerIntegration - Untrusted URLs', () => {
41 const {
42 resetModules,
43 itRenders,
44 + clientCleanRender,
45 clientRenderOnBadMarkup,
46 clientRenderOnServerString,
47 } = ReactDOMServerIntegrationUtils(initModules);
@@ -141,6 +142,11 @@ describe('ReactDOMServerIntegration - Untrusted URLs', () => {
142 });
143
144 itRenders('a javascript protocol frame src', async render => {
145 + if (render === clientCleanRender || render === clientRenderOnServerString) {
146 + // React does not hydrate framesets properly because the default hydration scope
147 + // is the body
148 + return;
149 + }
150 const e = await render(
151 <html>
152 <head />
packages/react-markup/src/ReactFizzConfigMarkup.js
+2
@@ -174,6 +174,7 @@ export function writeStartClientRenderedSuspenseBoundary(
174 export function writeEndCompletedSuspenseBoundary(
175 destination: Destination,
176 renderState: RenderState,
177 + preambleState: null | PreambleState,
178 ): boolean {
179 // Markup doesn't have any instructions.
180 return true;
@@ -181,6 +182,7 @@ export function writeEndCompletedSuspenseBoundary(
182 export function writeEndClientRenderedSuspenseBoundary(
183 destination: Destination,
184 renderState: RenderState,
185 + preambleState: null | PreambleState,
186 ): boolean {
187 // Markup doesn't have any instructions.
188 return true;
packages/react-reconciler/src/ReactFiberCommitHostEffects.js
+1
@@ -314,6 +314,7 @@ function insertOrAppendPlacementNodeIntoContainer(
314 // This singleton is the parent of deeper nodes and needs to become
315 // the parent for child insertions and appends
316 parent = node.stateNode;
317 + before = null;
318 }
319
320 const child = node.child;
packages/react-reconciler/src/ReactFiberConfigWithNoHydration.js
+2
@@ -28,9 +28,11 @@ export const registerSuspenseInstanceRetry = shim;
28 export const canHydrateFormStateMarker = shim;
29 export const isFormStateMarkerMatching = shim;
30 export const getNextHydratableSibling = shim;
31 +export const getNextHydratableSiblingAfterSingleton = shim;
32 export const getFirstHydratableChild = shim;
33 export const getFirstHydratableChildWithinContainer = shim;
34 export const getFirstHydratableChildWithinSuspenseInstance = shim;
35 +export const getFirstHydratableChildWithinSingleton = shim;
36 export const canHydrateInstance = shim;
37 export const canHydrateTextInstance = shim;
38 export const canHydrateSuspenseInstance = shim;
packages/react-reconciler/src/ReactFiberHydrationContext.js
+34 -21
@@ -37,9 +37,11 @@ import {
37 supportsHydration,
38 supportsSingletons,
39 getNextHydratableSibling,
40 + getNextHydratableSiblingAfterSingleton,
41 getFirstHydratableChild,
42 getFirstHydratableChildWithinContainer,
43 getFirstHydratableChildWithinSuspenseInstance,
44 + getFirstHydratableChildWithinSingleton,
45 hydrateInstance,
46 diffHydratedPropsForDevWarnings,
47 describeHydratableInstanceForDevWarnings,
@@ -366,7 +368,11 @@ function claimHydratableSingleton(fiber: Fiber): void {
368
369 hydrationParentFiber = fiber;
370 rootOrSingletonContext = true;
369 - nextHydratableInstance = getFirstHydratableChild(instance);
371 + nextHydratableInstance = getFirstHydratableChildWithinSingleton(
372 + fiber.type,
373 + instance,
374 + nextHydratableInstance,
375 + );
376 }
377 }
378
@@ -593,14 +599,14 @@ function popToNextHostParent(fiber: Fiber): void {
599 hydrationParentFiber = fiber.return;
600 while (hydrationParentFiber) {
601 switch (hydrationParentFiber.tag) {
596 - case HostRoot:
597 - case HostSingleton:
598 - rootOrSingletonContext = true;
599 - return;
602 case HostComponent:
603 case SuspenseComponent:
604 rootOrSingletonContext = false;
605 return;
606 + case HostSingleton:
607 + case HostRoot:
608 + rootOrSingletonContext = true;
609 + return;
610 default:
611 hydrationParentFiber = hydrationParentFiber.return;
612 }
@@ -625,20 +631,25 @@ function popHydrationState(fiber: Fiber): boolean {
631 return false;
632 }
633
628 - let shouldClear = false;
634 + const tag = fiber.tag;
635 +
636 if (supportsSingletons) {
637 // With float we never clear the Root, or Singleton instances. We also do not clear Instances
638 // that have singleton text content
639 if (
633 - fiber.tag !== HostRoot &&
634 - fiber.tag !== HostSingleton &&
640 + tag !== HostRoot &&
641 + tag !== HostSingleton &&
642 !(
636 - fiber.tag === HostComponent &&
643 + tag === HostComponent &&
644 (!shouldDeleteUnhydratedTailInstances(fiber.type) ||
645 shouldSetTextContent(fiber.type, fiber.memoizedProps))
646 )
647 ) {
641 - shouldClear = true;
648 + const nextInstance = nextHydratableInstance;
649 + if (nextInstance) {
650 + warnIfUnhydratedTailNodes(fiber);
651 + throwOnHydrationMismatch(fiber);
652 + }
653 }
654 } else {
655 // If we have any remaining hydratable nodes, we need to delete them now.
@@ -646,24 +657,26 @@ function popHydrationState(fiber: Fiber): boolean {
657 // other nodes in them. We also ignore components with pure text content in
658 // side of them. We also don't delete anything inside the root container.
659 if (
649 - fiber.tag !== HostRoot &&
650 - (fiber.tag !== HostComponent ||
660 + tag !== HostRoot &&
661 + (tag !== HostComponent ||
662 (shouldDeleteUnhydratedTailInstances(fiber.type) &&
663 !shouldSetTextContent(fiber.type, fiber.memoizedProps)))
664 ) {
654 - shouldClear = true;
655 - }
656 - }
657 - if (shouldClear) {
658 - const nextInstance = nextHydratableInstance;
659 - if (nextInstance) {
660 - warnIfUnhydratedTailNodes(fiber);
661 - throwOnHydrationMismatch(fiber);
665 + const nextInstance = nextHydratableInstance;
666 + if (nextInstance) {
667 + warnIfUnhydratedTailNodes(fiber);
668 + throwOnHydrationMismatch(fiber);
669 + }
670 }
671 }
672 popToNextHostParent(fiber);
665 - if (fiber.tag === SuspenseComponent) {
673 + if (tag === SuspenseComponent) {
674 nextHydratableInstance = skipPastDehydratedSuspenseInstance(fiber);
675 + } else if (supportsSingletons && tag === HostSingleton) {
676 + nextHydratableInstance = getNextHydratableSiblingAfterSingleton(
677 + fiber.type,
678 + nextHydratableInstance,
679 + );
680 } else {
681 nextHydratableInstance = hydrationParentFiber
682 ? getNextHydratableSibling(fiber.stateNode)
packages/react-reconciler/src/forks/ReactFiberConfig.custom.js
+4
@@ -174,11 +174,15 @@ export const registerSuspenseInstanceRetry =
174 export const canHydrateFormStateMarker = $$$config.canHydrateFormStateMarker;
175 export const isFormStateMarkerMatching = $$$config.isFormStateMarkerMatching;
176 export const getNextHydratableSibling = $$$config.getNextHydratableSibling;
177 +export const getNextHydratableSiblingAfterSingleton =
178 + $$$config.getNextHydratableSiblingAfterSingleton;
179 export const getFirstHydratableChild = $$$config.getFirstHydratableChild;
180 export const getFirstHydratableChildWithinContainer =
181 $$$config.getFirstHydratableChildWithinContainer;
182 export const getFirstHydratableChildWithinSuspenseInstance =
183 $$$config.getFirstHydratableChildWithinSuspenseInstance;
184 +export const getFirstHydratableChildWithinSingleton =
185 + $$$config.getFirstHydratableChildWithinSingleton;
186 export const canHydrateInstance = $$$config.canHydrateInstance;
187 export const canHydrateTextInstance = $$$config.canHydrateTextInstance;
188 export const canHydrateSuspenseInstance = $$$config.canHydrateSuspenseInstance;
packages/react-server/src/ReactFizzServer.js
+6 -1
@@ -4865,6 +4865,7 @@ function flushSegment(
4865 return writeEndClientRenderedSuspenseBoundary(
4866 destination,
4867 request.renderState,
4868 + boundary.fallbackPreamble,
4869 );
4870 } else if (boundary.status !== COMPLETED) {
4871 if (boundary.status === PENDING) {
@@ -4935,7 +4936,11 @@ function flushSegment(
4936 const contentSegment = completedSegments[0];
4937 flushSegment(request, destination, contentSegment, hoistableState);
4938
4938 - return writeEndCompletedSuspenseBoundary(destination, request.renderState);
4939 + return writeEndCompletedSuspenseBoundary(
4940 + destination,
4941 + request.renderState,
4942 + boundary.contentPreamble,
4943 + );
4944 }
4945 }
4946