diff --git a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
index 9e3574885fa6e..89d5657343a11 100644
--- a/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+++ b/packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
@@ -207,6 +207,9 @@ const SUSPENSE_START_DATA = '$';
const SUSPENSE_END_DATA = '/$';
const SUSPENSE_PENDING_START_DATA = '$?';
const SUSPENSE_FALLBACK_START_DATA = '$!';
+const PREAMBLE_CONTRIBUTION_HTML = 0b001;
+const PREAMBLE_CONTRIBUTION_BODY = 0b010;
+const PREAMBLE_CONTRIBUTION_HEAD = 0b100;
const FORM_STATE_IS_MATCHING = 'F!';
const FORM_STATE_IS_NOT_MATCHING = 'F';
@@ -973,6 +976,35 @@ export function clearSuspenseBoundary(
if (nextNode && nextNode.nodeType === COMMENT_NODE) {
const data = ((nextNode: any).data: string);
if (data === SUSPENSE_END_DATA) {
+ if (node.nodeType === COMMENT_NODE) {
+ // The previous node was a comment. it is assumed to be a preamble marker comment because
+ const code: number = (node: any).data.charCodeAt(0) - 48;
+ if (code > 0 && code < 8) {
+ // It's not normally possible to insert a comment immediately preceding Suspense boundary
+ // closing comment marker so we can infer that if the comment preceding starts with "1" through "7"
+ // then it is in fact a preamble contribution marker comment. We do this value test to avoid the case
+ // where the Suspense boundary is empty and the preceding comment marker is the Suspense boundary
+ // opening marker or the closing marker of an inner boundary. In those cases the first character won't
+ // have the requisite value to be interpretted as a Preamble contribution
+ }
+ const ownerDocument = parentInstance.ownerDocument;
+ if (code & PREAMBLE_CONTRIBUTION_HTML) {
+ const documentElement: Element =
+ (ownerDocument.documentElement: any);
+ releaseSingletonInstance(documentElement);
+ }
+ if (code & PREAMBLE_CONTRIBUTION_BODY) {
+ const body: Element = (ownerDocument.body: any);
+ releaseSingletonInstance(body);
+ }
+ if (code & PREAMBLE_CONTRIBUTION_HEAD) {
+ const head: Element = (ownerDocument.head: any);
+ releaseSingletonInstance(head);
+ // We need to clear the head because this is the only singleton that can have children that
+ // were part of this boundary but are not inside this boundary.
+ clearHead(head);
+ }
+ }
if (depth === 0) {
parentInstance.removeChild(nextNode);
// Retry if any event replaying was blocked on this.
@@ -1501,7 +1533,7 @@ function clearContainerSparingly(container: Node) {
case 'STYLE': {
continue;
}
- // Stylesheet tags are retained because tehy may likely come from 3rd party scripts and extensions
+ // Stylesheet tags are retained because they may likely come from 3rd party scripts and extensions
case 'LINK': {
if (((node: any): HTMLLinkElement).rel.toLowerCase() === 'stylesheet') {
continue;
@@ -1513,6 +1545,27 @@ function clearContainerSparingly(container: Node) {
return;
}
+function clearHead(head: Element): void {
+ let node = head.firstChild;
+ while (node) {
+ const nextNode = node.nextSibling;
+ const nodeName = node.nodeName;
+ if (
+ isMarkedHoistable(node) ||
+ nodeName === 'SCRIPT' ||
+ nodeName === 'STYLE' ||
+ (nodeName === 'LINK' &&
+ ((node: any): HTMLLinkElement).rel.toLowerCase() === 'stylesheet')
+ ) {
+ // retain these nodes
+ } else {
+ head.removeChild(node);
+ }
+ node = nextNode;
+ }
+ return;
+}
+
// Making this so we can eventually move all of the instance caching to the commit phase.
// Currently this is only used to associate fiber and props to instances for hydrating
// HostSingletons. The reason we need it here is we only want to make this binding on commit
@@ -1874,7 +1927,20 @@ export function getFirstHydratableChild(
export function getFirstHydratableChildWithinContainer(
parentContainer: Container,
): null | HydratableInstance {
- return getNextHydratable(parentContainer.firstChild);
+ let parentElement: Element;
+ switch (parentContainer.nodeType) {
+ case DOCUMENT_NODE:
+ parentElement = (parentContainer: any).body;
+ break;
+ default: {
+ if (parentContainer.nodeName === 'HTML') {
+ parentElement = (parentContainer: any).ownerDocument.body;
+ } else {
+ parentElement = (parentContainer: any);
+ }
+ }
+ }
+ return getNextHydratable(parentElement.firstChild);
}
export function getFirstHydratableChildWithinSuspenseInstance(
@@ -1883,6 +1949,40 @@ export function getFirstHydratableChildWithinSuspenseInstance(
return getNextHydratable(parentInstance.nextSibling);
}
+// If it were possible to have more than one scope singleton in a DOM tree
+// we would need to model this as a stack but since you can only have one
+// and head is the only singleton that is a scope in DOM we can get away with
+// tracking this as a single value.
+let previousHydratableOnEnteringScopedSingleton: null | HydratableInstance =
+ null;
+
+export function getFirstHydratableChildWithinSingleton(
+ type: string,
+ singletonInstance: Instance,
+ currentHydratableInstance: null | HydratableInstance,
+): null | HydratableInstance {
+ if (isSingletonScope(type)) {
+ previousHydratableOnEnteringScopedSingleton = currentHydratableInstance;
+ return getNextHydratable(singletonInstance.firstChild);
+ } else {
+ return currentHydratableInstance;
+ }
+}
+
+export function getNextHydratableSiblingAfterSingleton(
+ type: string,
+ currentHydratableInstance: null | HydratableInstance,
+): null | HydratableInstance {
+ if (isSingletonScope(type)) {
+ const previousHydratableInstance =
+ previousHydratableOnEnteringScopedSingleton;
+ previousHydratableOnEnteringScopedSingleton = null;
+ return previousHydratableInstance;
+ } else {
+ return currentHydratableInstance;
+ }
+}
+
export function describeHydratableInstanceForDevWarnings(
instance: HydratableInstance,
): string | {type: string, props: $ReadOnly} {
diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
index 4b8841a06e67b..8fac981ab29f2 100644
--- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
+++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
@@ -684,16 +684,23 @@ export function completeResumableState(resumableState: ResumableState): void {
resumableState.bootstrapModules = undefined;
}
+const NoContribution /* */ = 0b000;
+const HTMLContribution /* */ = 0b001;
+const BodyContribution /* */ = 0b010;
+const HeadContribution /* */ = 0b100;
+
export type PreambleState = {
htmlChunks: null | Array,
headChunks: null | Array,
bodyChunks: null | Array,
+ contribution: number,
};
export function createPreambleState(): PreambleState {
return {
htmlChunks: null,
headChunks: null,
bodyChunks: null,
+ contribution: NoContribution,
};
}
@@ -3227,7 +3234,7 @@ function pushStartHead(
throw new Error(`The ${'``'} tag may only be rendered once.`);
}
preamble.headChunks = [];
- return pushStartGenericElement(preamble.headChunks, props, 'head');
+ return pushStartSingletonElement(preamble.headChunks, props, 'head');
} else {
// This is deep and is likely just an error. we emit it inline though.
// Validation should warn that this tag is the the wrong spot.
@@ -3251,7 +3258,7 @@ function pushStartBody(
}
preamble.bodyChunks = [];
- return pushStartGenericElement(preamble.bodyChunks, props, 'body');
+ return pushStartSingletonElement(preamble.bodyChunks, props, 'body');
} else {
// This is deep and is likely just an error. we emit it inline though.
// Validation should warn that this tag is the the wrong spot.
@@ -3275,7 +3282,7 @@ function pushStartHtml(
}
preamble.htmlChunks = [DOCTYPE];
- return pushStartGenericElement(preamble.htmlChunks, props, 'html');
+ return pushStartSingletonElement(preamble.htmlChunks, props, 'html');
} else {
// This is deep and is likely just an error. we emit it inline though.
// Validation should warn that this tag is the the wrong spot.
@@ -3416,6 +3423,43 @@ function pushScriptImpl(
return null;
}
+// This is a fork of pushStartGenericElement because we don't ever want to do
+// the children as strign optimization on that path when rendering singletons.
+// When we eliminate that special path we can delete this fork and unify it again
+function pushStartSingletonElement(
+ target: Array,
+ props: Object,
+ tag: string,
+): ReactNodeList {
+ target.push(startChunkForTag(tag));
+
+ let children = null;
+ let innerHTML = null;
+ for (const propKey in props) {
+ if (hasOwnProperty.call(props, propKey)) {
+ const propValue = props[propKey];
+ if (propValue == null) {
+ continue;
+ }
+ switch (propKey) {
+ case 'children':
+ children = propValue;
+ break;
+ case 'dangerouslySetInnerHTML':
+ innerHTML = propValue;
+ break;
+ default:
+ pushAttribute(target, propKey, propValue);
+ break;
+ }
+ }
+ }
+
+ target.push(endOfStartTag);
+ pushInnerHTML(target, innerHTML, children);
+ return children;
+}
+
function pushStartGenericElement(
target: Array,
props: Object,
@@ -3907,14 +3951,17 @@ export function hoistPreambleState(
preambleState: PreambleState,
) {
const rootPreamble = renderState.preamble;
- if (rootPreamble.htmlChunks === null) {
+ if (rootPreamble.htmlChunks === null && preambleState.htmlChunks) {
rootPreamble.htmlChunks = preambleState.htmlChunks;
+ preambleState.contribution |= HTMLContribution;
}
- if (rootPreamble.headChunks === null) {
+ if (rootPreamble.headChunks === null && preambleState.headChunks) {
rootPreamble.headChunks = preambleState.headChunks;
+ preambleState.contribution |= HeadContribution;
}
- if (rootPreamble.bodyChunks === null) {
+ if (rootPreamble.bodyChunks === null && preambleState.bodyChunks) {
rootPreamble.bodyChunks = preambleState.bodyChunks;
+ preambleState.contribution |= BodyContribution;
}
}
@@ -4091,7 +4138,11 @@ export function writeStartClientRenderedSuspenseBoundary(
export function writeEndCompletedSuspenseBoundary(
destination: Destination,
renderState: RenderState,
+ preambleState: null | PreambleState,
): boolean {
+ if (preambleState) {
+ writePreambleContribution(destination, preambleState);
+ }
return writeChunkAndReturn(destination, endSuspenseBoundary);
}
export function writeEndPendingSuspenseBoundary(
@@ -4103,10 +4154,31 @@ export function writeEndPendingSuspenseBoundary(
export function writeEndClientRenderedSuspenseBoundary(
destination: Destination,
renderState: RenderState,
+ preambleState: null | PreambleState,
): boolean {
+ if (preambleState) {
+ writePreambleContribution(destination, preambleState);
+ }
return writeChunkAndReturn(destination, endSuspenseBoundary);
}
+const boundaryPreambleContributionChunkStart = stringToPrecomputedChunk('');
+
+function writePreambleContribution(
+ destination: Destination,
+ preambleState: PreambleState,
+) {
+ const contribution = preambleState.contribution;
+ if (contribution !== NoContribution) {
+ writeChunk(destination, boundaryPreambleContributionChunkStart);
+ // This is a number type so we can do the fast path without coercion checking
+ // eslint-disable-next-line react-internal/safe-string-coercion
+ writeChunk(destination, stringToChunk('' + contribution));
+ writeChunk(destination, boundaryPreambleContributionChunkEnd);
+ }
+}
+
const startSegmentHTML = stringToPrecomputedChunk('
+
+
+ ,
+ );
+ if (gate(flags => flags.enableOwnerStacks)) {
+ assertConsoleErrorDev([
+ [
+ 'Cannot render a 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 remove the `itemProp` prop. Otherwise, try moving this tag into the or of the Document.',
+ {withoutStack: true},
+ ],
+ 'In HTML, cannot be a child of .\nThis will cause a hydration error.' +
+ '\n' +
+ '\n ' +
+ '\n> ' +
+ '\n ' +
+ '\n ' +
+ '\n> ' +
+ '\n ...' +
+ '\n' +
+ '\n in meta (at **)' +
+ '\n in App (at **)',
+ ' cannot contain a nested .\nSee this log for the ancestor stack trace.' +
+ '\n in html (at **)' +
+ '\n in App (at **)',
+ [
+ 'Cannot render a 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 remove the `itemProp` prop. Otherwise, try moving this tag into the or of the Document.',
+ {withoutStack: true},
+ ],
+ ]);
+ } else {
+ assertConsoleErrorDev([
+ 'Cannot render a 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 remove the `itemProp` prop. Otherwise, try moving this tag into the or of the Document.' +
+ '\n in Suspense (at **)' +
+ '\n in html (at **)' +
+ '\n in App (at **)',
+ 'In HTML, cannot be a child of .\nThis will cause a hydration error.' +
+ '\n' +
+ '\n ' +
+ '\n> ' +
+ '\n ' +
+ '\n ' +
+ '\n> ' +
+ '\n ...' +
+ '\n' +
+ '\n in meta (at **)' +
+ '\n in Suspense (at **)' +
+ '\n in html (at **)' +
+ '\n in App (at **)',
+ 'Cannot render a 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 remove the `itemProp` prop. Otherwise, try moving this tag into the or of the Document.' +
+ '\n in Suspense (at **)' +
+ '\n in html (at **)' +
+ '\n in App (at **)',
+ ]);
+ }
+
+ await root.unmount();
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+ ,
+ );
});
- it('can server render Suspense before, after, and around ', async () => {
+ it('can render Suspense before, after, and around ', async () => {
function BlockedOn({value, children}) {
readText(value);
return children;
@@ -9119,11 +9217,90 @@ describe('ReactDOMFizzServer', () => {
,
);
+
+ const root = ReactDOMClient.hydrateRoot(document, );
+ await waitForAll([]);
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+
+
+
+
+
+
+
hello world
+
+ ,
+ );
+ if (gate(flags => flags.enableOwnerStacks)) {
+ assertConsoleErrorDev([
+ [
+ 'Cannot render a 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 remove the `itemProp` prop. Otherwise, try moving this tag into the or of the Document.',
+ {withoutStack: true},
+ ],
+ 'In HTML, cannot be a child of .\nThis will cause a hydration error.' +
+ '\n' +
+ '\n ' +
+ '\n> ' +
+ '\n ' +
+ '\n ' +
+ '\n> ' +
+ '\n ...' +
+ '\n' +
+ '\n in meta (at **)' +
+ '\n in App (at **)',
+ ' cannot contain a nested .\nSee this log for the ancestor stack trace.' +
+ '\n in html (at **)' +
+ '\n in App (at **)',
+ [
+ 'Cannot render a 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 remove the `itemProp` prop. Otherwise, try moving this tag into the or of the Document.',
+ {withoutStack: true},
+ ],
+ ]);
+ } else {
+ assertConsoleErrorDev([
+ 'Cannot render a 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 remove the `itemProp` prop. Otherwise, try moving this tag into the or of the Document.' +
+ '\n in Suspense (at **)' +
+ '\n in html (at **)' +
+ '\n in App (at **)',
+ 'In HTML, cannot be a child of .\nThis will cause a hydration error.' +
+ '\n' +
+ '\n ' +
+ '\n> ' +
+ '\n ' +
+ '\n ' +
+ '\n> ' +
+ '\n ...' +
+ '\n' +
+ '\n in meta (at **)' +
+ '\n in Suspense (at **)' +
+ '\n in html (at **)' +
+ '\n in App (at **)',
+ 'Cannot render a 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 remove the `itemProp` prop. Otherwise, try moving this tag into the or of the Document.' +
+ '\n in Suspense (at **)' +
+ '\n in html (at **)' +
+ '\n in App (at **)',
+ ]);
+ }
+
+ await root.unmount();
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+ ,
+ );
});
- it('will render fallback Document when erroring a boundary above the body', async () => {
+ it('will render fallback Document when erroring a boundary above the body and recover on the client', async () => {
+ let serverRendering = true;
function Boom() {
- throw new Error('Boom!');
+ if (serverRendering) {
+ throw new Error('Boom!');
+ }
+ return null;
}
function App() {
@@ -9174,11 +9351,50 @@ describe('ReactDOMFizzServer', () => {
,
);
+
+ serverRendering = false;
+
+ const recoverableErrors = [];
+ const root = ReactDOMClient.hydrateRoot(document, , {
+ onRecoverableError(err) {
+ recoverableErrors.push(err);
+ },
+ });
+ await waitForAll([]);
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+ hello world
+
+ ,
+ );
+ expect(recoverableErrors).toEqual([
+ __DEV__
+ ? new Error(
+ 'Switched to client rendering because the server rendering errored:\n\nBoom!',
+ )
+ : new Error(
+ 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
+ ),
+ ]);
+
+ root.unmount();
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+ ,
+ );
});
it('will hoist resources and hositables from a primary tree into the of a client rendered fallback', async () => {
+ let serverRendering = true;
function Boom() {
- throw new Error('Boom!');
+ if (serverRendering) {
+ throw new Error('Boom!');
+ }
+ return null;
}
function App() {
@@ -9255,6 +9471,65 @@ describe('ReactDOMFizzServer', () => {
,
);
+
+ serverRendering = false;
+
+ const recoverableErrors = [];
+ const root = ReactDOMClient.hydrateRoot(document, , {
+ onRecoverableError(err) {
+ recoverableErrors.push(err);
+ },
+ });
+ await waitForAll([]);
+ expect(recoverableErrors).toEqual([
+ __DEV__
+ ? new Error(
+ 'Switched to client rendering because the server rendering errored:\n\nBoom!',
+ )
+ : new Error(
+ 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
+ ),
+ ]);
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+
+
+
+
+
+ hello world
+
+ ,
+ );
+
+ root.unmount();
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+
+
+
+ ,
+ );
});
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', () => {
});
it('Can render a fallback alongside a non-fallback body', async () => {
+ let serverRendering = true;
function Boom() {
- throw new Error('Boom!');
+ if (serverRendering) {
+ throw new Error('Boom!');
+ }
+ return null;
}
function App() {
@@ -9416,11 +9695,52 @@ describe('ReactDOMFizzServer', () => {
,
);
+
+ serverRendering = false;
+
+ const recoverableErrors = [];
+ const root = ReactDOMClient.hydrateRoot(document, , {
+ onRecoverableError(err) {
+ recoverableErrors.push(err);
+ },
+ });
+ await waitForAll([]);
+ expect(recoverableErrors).toEqual([
+ __DEV__
+ ? new Error(
+ 'Switched to client rendering because the server rendering errored:\n\nBoom!',
+ )
+ : new Error(
+ 'The server could not finish this Suspense boundary, likely due to an error during server rendering. Switched to client rendering.',
+ ),
+ ]);
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+
+
+
+ ,
+ );
});
it('will flush the preamble as soon as a complete preamble is available', async () => {
@@ -9740,5 +10140,177 @@ describe('ReactDOMFizzServer', () => {
,
);
+
+ const root = ReactDOMClient.hydrateRoot(document, );
+ await waitForAll([]);
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+
+
+ ,
+ );
+ });
+
+ it('will clean up the head when a hydration mismatch causes a boundary to recover on the client', async () => {
+ let content = 'server';
+
+ function ServerApp() {
+ return (
+
+
+
+
+
+
{content}
+
+
+ );
+ }
+
+ function ClientApp() {
+ return (
+
+
+
+
+
+ {content}
+
+
+ );
+ }
+
+ await act(() => {
+ const {pipe} = renderToPipeableStream();
+ pipe(writable);
+ });
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+
+ server
+ ,
+ );
+
+ content = 'client';
+
+ const recoverableErrors = [];
+ const root = ReactDOMClient.hydrateRoot(document, , {
+ onRecoverableError(err) {
+ recoverableErrors.push(err.message);
+ },
+ });
+ await waitForAll([]);
+ if (gate(flags => flags.favorSafetyOverHydrationPerf)) {
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+
+ client
+ ,
+ );
+ expect(recoverableErrors).toEqual([
+ expect.stringContaining(
+ "Hydration failed because the server rendered HTML didn't match the client.",
+ ),
+ ]);
+ } else {
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+
+ server
+ ,
+ );
+ expect(recoverableErrors).toEqual([]);
+ assertConsoleErrorDev([
+ "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:" +
+ '\n' +
+ "\n- A server/client branch `if (typeof window !== 'undefined')`." +
+ "\n- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called." +
+ "\n- Date formatting in a user's locale which doesn't match the server." +
+ '\n- External changing data without sending a snapshot of it along with the HTML.' +
+ '\n- Invalid HTML tag nesting.' +
+ '\n' +
+ '\nIt can also happen if the client has a browser extension installed which messes with the HTML before React loaded.' +
+ '\n' +
+ '\nhttps://react.dev/link/hydration-mismatch' +
+ '\n' +
+ '\n ' +
+ '\n ' +
+ '\n ' +
+ '\n ' +
+ '\n ' +
+ '\n ' +
+ '\n+ client' +
+ '\n- server' +
+ '\n+ client' +
+ '\n- server' +
+ '\n' +
+ '\n in Suspense (at **)' +
+ '\n in ClientApp (at **)',
+ ]);
+ }
+
+ root.unmount();
+ expect(getVisibleChildren(document)).toEqual(
+
+
+
+ ,
+ );
});
});
diff --git a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationUntrustedURL-test.js b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationUntrustedURL-test.js
index 384e8beb0b214..8664904130c11 100644
--- a/packages/react-dom/src/__tests__/ReactDOMServerIntegrationUntrustedURL-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMServerIntegrationUntrustedURL-test.js
@@ -41,6 +41,7 @@ describe('ReactDOMServerIntegration - Untrusted URLs', () => {
const {
resetModules,
itRenders,
+ clientCleanRender,
clientRenderOnBadMarkup,
clientRenderOnServerString,
} = ReactDOMServerIntegrationUtils(initModules);
@@ -141,6 +142,11 @@ describe('ReactDOMServerIntegration - Untrusted URLs', () => {
});
itRenders('a javascript protocol frame src', async render => {
+ if (render === clientCleanRender || render === clientRenderOnServerString) {
+ // React does not hydrate framesets properly because the default hydration scope
+ // is the body
+ return;
+ }
const e = await render(
diff --git a/packages/react-markup/src/ReactFizzConfigMarkup.js b/packages/react-markup/src/ReactFizzConfigMarkup.js
index 99e9921c8190a..358a08e7c54b8 100644
--- a/packages/react-markup/src/ReactFizzConfigMarkup.js
+++ b/packages/react-markup/src/ReactFizzConfigMarkup.js
@@ -174,6 +174,7 @@ export function writeStartClientRenderedSuspenseBoundary(
export function writeEndCompletedSuspenseBoundary(
destination: Destination,
renderState: RenderState,
+ preambleState: null | PreambleState,
): boolean {
// Markup doesn't have any instructions.
return true;
@@ -181,6 +182,7 @@ export function writeEndCompletedSuspenseBoundary(
export function writeEndClientRenderedSuspenseBoundary(
destination: Destination,
renderState: RenderState,
+ preambleState: null | PreambleState,
): boolean {
// Markup doesn't have any instructions.
return true;
diff --git a/packages/react-reconciler/src/ReactFiberCommitHostEffects.js b/packages/react-reconciler/src/ReactFiberCommitHostEffects.js
index 159c12bd4bcf2..c104c2a8464b5 100644
--- a/packages/react-reconciler/src/ReactFiberCommitHostEffects.js
+++ b/packages/react-reconciler/src/ReactFiberCommitHostEffects.js
@@ -314,6 +314,7 @@ function insertOrAppendPlacementNodeIntoContainer(
// This singleton is the parent of deeper nodes and needs to become
// the parent for child insertions and appends
parent = node.stateNode;
+ before = null;
}
const child = node.child;
diff --git a/packages/react-reconciler/src/ReactFiberConfigWithNoHydration.js b/packages/react-reconciler/src/ReactFiberConfigWithNoHydration.js
index 3707f99f488fb..0bb85246dfe24 100644
--- a/packages/react-reconciler/src/ReactFiberConfigWithNoHydration.js
+++ b/packages/react-reconciler/src/ReactFiberConfigWithNoHydration.js
@@ -28,9 +28,11 @@ export const registerSuspenseInstanceRetry = shim;
export const canHydrateFormStateMarker = shim;
export const isFormStateMarkerMatching = shim;
export const getNextHydratableSibling = shim;
+export const getNextHydratableSiblingAfterSingleton = shim;
export const getFirstHydratableChild = shim;
export const getFirstHydratableChildWithinContainer = shim;
export const getFirstHydratableChildWithinSuspenseInstance = shim;
+export const getFirstHydratableChildWithinSingleton = shim;
export const canHydrateInstance = shim;
export const canHydrateTextInstance = shim;
export const canHydrateSuspenseInstance = shim;
diff --git a/packages/react-reconciler/src/ReactFiberHydrationContext.js b/packages/react-reconciler/src/ReactFiberHydrationContext.js
index b4d948e735276..23f13bbcadbf8 100644
--- a/packages/react-reconciler/src/ReactFiberHydrationContext.js
+++ b/packages/react-reconciler/src/ReactFiberHydrationContext.js
@@ -37,9 +37,11 @@ import {
supportsHydration,
supportsSingletons,
getNextHydratableSibling,
+ getNextHydratableSiblingAfterSingleton,
getFirstHydratableChild,
getFirstHydratableChildWithinContainer,
getFirstHydratableChildWithinSuspenseInstance,
+ getFirstHydratableChildWithinSingleton,
hydrateInstance,
diffHydratedPropsForDevWarnings,
describeHydratableInstanceForDevWarnings,
@@ -366,7 +368,11 @@ function claimHydratableSingleton(fiber: Fiber): void {
hydrationParentFiber = fiber;
rootOrSingletonContext = true;
- nextHydratableInstance = getFirstHydratableChild(instance);
+ nextHydratableInstance = getFirstHydratableChildWithinSingleton(
+ fiber.type,
+ instance,
+ nextHydratableInstance,
+ );
}
}
@@ -593,14 +599,14 @@ function popToNextHostParent(fiber: Fiber): void {
hydrationParentFiber = fiber.return;
while (hydrationParentFiber) {
switch (hydrationParentFiber.tag) {
- case HostRoot:
- case HostSingleton:
- rootOrSingletonContext = true;
- return;
case HostComponent:
case SuspenseComponent:
rootOrSingletonContext = false;
return;
+ case HostSingleton:
+ case HostRoot:
+ rootOrSingletonContext = true;
+ return;
default:
hydrationParentFiber = hydrationParentFiber.return;
}
@@ -625,20 +631,25 @@ function popHydrationState(fiber: Fiber): boolean {
return false;
}
- let shouldClear = false;
+ const tag = fiber.tag;
+
if (supportsSingletons) {
// With float we never clear the Root, or Singleton instances. We also do not clear Instances
// that have singleton text content
if (
- fiber.tag !== HostRoot &&
- fiber.tag !== HostSingleton &&
+ tag !== HostRoot &&
+ tag !== HostSingleton &&
!(
- fiber.tag === HostComponent &&
+ tag === HostComponent &&
(!shouldDeleteUnhydratedTailInstances(fiber.type) ||
shouldSetTextContent(fiber.type, fiber.memoizedProps))
)
) {
- shouldClear = true;
+ const nextInstance = nextHydratableInstance;
+ if (nextInstance) {
+ warnIfUnhydratedTailNodes(fiber);
+ throwOnHydrationMismatch(fiber);
+ }
}
} else {
// If we have any remaining hydratable nodes, we need to delete them now.
@@ -646,24 +657,26 @@ function popHydrationState(fiber: Fiber): boolean {
// other nodes in them. We also ignore components with pure text content in
// side of them. We also don't delete anything inside the root container.
if (
- fiber.tag !== HostRoot &&
- (fiber.tag !== HostComponent ||
+ tag !== HostRoot &&
+ (tag !== HostComponent ||
(shouldDeleteUnhydratedTailInstances(fiber.type) &&
!shouldSetTextContent(fiber.type, fiber.memoizedProps)))
) {
- shouldClear = true;
- }
- }
- if (shouldClear) {
- const nextInstance = nextHydratableInstance;
- if (nextInstance) {
- warnIfUnhydratedTailNodes(fiber);
- throwOnHydrationMismatch(fiber);
+ const nextInstance = nextHydratableInstance;
+ if (nextInstance) {
+ warnIfUnhydratedTailNodes(fiber);
+ throwOnHydrationMismatch(fiber);
+ }
}
}
popToNextHostParent(fiber);
- if (fiber.tag === SuspenseComponent) {
+ if (tag === SuspenseComponent) {
nextHydratableInstance = skipPastDehydratedSuspenseInstance(fiber);
+ } else if (supportsSingletons && tag === HostSingleton) {
+ nextHydratableInstance = getNextHydratableSiblingAfterSingleton(
+ fiber.type,
+ nextHydratableInstance,
+ );
} else {
nextHydratableInstance = hydrationParentFiber
? getNextHydratableSibling(fiber.stateNode)
diff --git a/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js b/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js
index 91420ca88cd95..ee5f40ad829ad 100644
--- a/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js
+++ b/packages/react-reconciler/src/forks/ReactFiberConfig.custom.js
@@ -174,11 +174,15 @@ export const registerSuspenseInstanceRetry =
export const canHydrateFormStateMarker = $$$config.canHydrateFormStateMarker;
export const isFormStateMarkerMatching = $$$config.isFormStateMarkerMatching;
export const getNextHydratableSibling = $$$config.getNextHydratableSibling;
+export const getNextHydratableSiblingAfterSingleton =
+ $$$config.getNextHydratableSiblingAfterSingleton;
export const getFirstHydratableChild = $$$config.getFirstHydratableChild;
export const getFirstHydratableChildWithinContainer =
$$$config.getFirstHydratableChildWithinContainer;
export const getFirstHydratableChildWithinSuspenseInstance =
$$$config.getFirstHydratableChildWithinSuspenseInstance;
+export const getFirstHydratableChildWithinSingleton =
+ $$$config.getFirstHydratableChildWithinSingleton;
export const canHydrateInstance = $$$config.canHydrateInstance;
export const canHydrateTextInstance = $$$config.canHydrateTextInstance;
export const canHydrateSuspenseInstance = $$$config.canHydrateSuspenseInstance;
diff --git a/packages/react-server/src/ReactFizzServer.js b/packages/react-server/src/ReactFizzServer.js
index 4c02d06e600a9..041ef31e2bd3d 100644
--- a/packages/react-server/src/ReactFizzServer.js
+++ b/packages/react-server/src/ReactFizzServer.js
@@ -4865,6 +4865,7 @@ function flushSegment(
return writeEndClientRenderedSuspenseBoundary(
destination,
request.renderState,
+ boundary.fallbackPreamble,
);
} else if (boundary.status !== COMPLETED) {
if (boundary.status === PENDING) {
@@ -4935,7 +4936,11 @@ function flushSegment(
const contentSegment = completedSegments[0];
flushSegment(request, destination, contentSegment, hoistableState);
- return writeEndCompletedSuspenseBoundary(destination, request.renderState);
+ return writeEndCompletedSuspenseBoundary(
+ destination,
+ request.renderState,
+ boundary.contentPreamble,
+ );
}
}