Skip to content

Commit fc2b823

Browse files
committed
Implement renderIntoDocument
This commit adds the function renderIntoDocument in react-dom/server and adds the ability to embed the rendered children in the necessary html tags to repereset a full document. this means you can render "<html>...</html>" or "<div>...</div>" and either way the render will emit html, head, and body tags as necessary to describe a valid and complete HTML page. Like renderIntoContainer, renderIntoDocument provides a stream immediately. While there is a shell of sorts this fucntion will start writing content from the preamble (html and head tags, plus resources that flush in the head) before finishing the shell. Additionally renderIntoContainer accepts fallback children and fallback bootstrap script options. If the Shell errors the fallback children will render instead of children. The expectation is that the client will attempt to render fresh on the client.
1 parent 8be28e5 commit fc2b823

35 files changed

Lines changed: 646 additions & 24 deletions

packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js

Lines changed: 158 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@ const DataStreamingFormat: StreamingFormat = 1;
123123
export type ResponseState = {
124124
bootstrapChunks: Array<Chunk | PrecomputedChunk>,
125125
fallbackBootstrapChunks: void | Array<Chunk | PrecomputedChunk>,
126+
requiresEmbedding: boolean,
127+
hasHead: boolean,
128+
hasHtml: boolean,
126129
placeholderPrefix: PrecomputedChunk,
127130
segmentPrefix: PrecomputedChunk,
128131
boundaryPrefix: string,
@@ -199,6 +202,7 @@ export function createResponseState(
199202
> | void,
200203
externalRuntimeConfig: string | BootstrapScriptDescriptor | void,
201204
containerID: string | void,
205+
documentEmbedding: boolean | void,
202206
): ResponseState {
203207
const idPrefix = identifierPrefix === undefined ? '' : identifierPrefix;
204208
const inlineScriptWithNonce =
@@ -335,6 +339,9 @@ export function createResponseState(
335339
fallbackBootstrapChunks: fallbackBootstrapChunks.length
336340
? fallbackBootstrapChunks
337341
: undefined,
342+
requiresEmbedding: documentEmbedding === true,
343+
hasHead: false,
344+
hasHtml: false,
338345
placeholderPrefix: stringToPrecomputedChunk(idPrefix + 'P:'),
339346
segmentPrefix: stringToPrecomputedChunk(idPrefix + 'S:'),
340347
boundaryPrefix: idPrefix + 'B:',
@@ -1660,33 +1667,100 @@ function pushStartHead(
16601667
target: Array<Chunk | PrecomputedChunk>,
16611668
preamble: Array<Chunk | PrecomputedChunk>,
16621669
props: Object,
1663-
tag: string,
16641670
responseState: ResponseState,
16651671
): ReactNodeList {
1666-
return pushStartGenericElement(
1667-
enableFloat ? preamble : target,
1668-
props,
1669-
tag,
1670-
responseState,
1671-
);
1672+
if (enableFloat) {
1673+
let children = null;
1674+
let innerHTML = null;
1675+
let includedAttributeProps = false;
1676+
1677+
if (!responseState.hasHead) {
1678+
responseState.hasHead = true;
1679+
preamble.push(startChunkForTag('head'));
1680+
for (const propKey in props) {
1681+
if (hasOwnProperty.call(props, propKey)) {
1682+
const propValue = props[propKey];
1683+
if (propValue == null) {
1684+
continue;
1685+
}
1686+
switch (propKey) {
1687+
case 'children':
1688+
children = propValue;
1689+
break;
1690+
case 'dangerouslySetInnerHTML':
1691+
innerHTML = propValue;
1692+
break;
1693+
default:
1694+
if (__DEV__) {
1695+
includedAttributeProps = true;
1696+
}
1697+
pushAttribute(preamble, responseState, propKey, propValue);
1698+
break;
1699+
}
1700+
}
1701+
}
1702+
preamble.push(endOfStartTag);
1703+
} else {
1704+
// We elide the actual <head> tag because it was previously rendered but we still need
1705+
// to render children/innerHTML
1706+
for (const propKey in props) {
1707+
if (hasOwnProperty.call(props, propKey)) {
1708+
const propValue = props[propKey];
1709+
if (propValue == null) {
1710+
continue;
1711+
}
1712+
switch (propKey) {
1713+
case 'children':
1714+
children = propValue;
1715+
break;
1716+
case 'dangerouslySetInnerHTML':
1717+
innerHTML = propValue;
1718+
break;
1719+
default:
1720+
if (__DEV__) {
1721+
includedAttributeProps = true;
1722+
}
1723+
break;
1724+
}
1725+
}
1726+
}
1727+
}
1728+
1729+
if (__DEV__) {
1730+
if ((responseState: any).isDocumentEmbedded && includedAttributeProps) {
1731+
// We use this embedded flag a heuristic for whether we are rendering with renderIntoDocument
1732+
console.error(
1733+
'A <head> tag was rendered with props when using "renderIntoDocument". In this rendering mode' +
1734+
' React may emit the head tag early in some circumstances and therefore props on the <head> tag are not' +
1735+
' supported and may be missing in the rendered output for any particular render. In many cases props that' +
1736+
' are set on a <head> tag can be set on the <html> tag instead.',
1737+
);
1738+
}
1739+
}
1740+
1741+
pushInnerHTML(target, innerHTML, children);
1742+
return children;
1743+
} else {
1744+
return pushStartGenericElement(target, props, 'head', responseState);
1745+
}
16721746
}
16731747

16741748
function pushStartHtml(
16751749
target: Array<Chunk | PrecomputedChunk>,
16761750
preamble: Array<Chunk | PrecomputedChunk>,
16771751
props: Object,
1678-
tag: string,
16791752
responseState: ResponseState,
16801753
formatContext: FormatContext,
16811754
): ReactNodeList {
1755+
responseState.hasHtml = true;
16821756
target = enableFloat ? preamble : target;
16831757
if (formatContext.insertionMode === ROOT_HTML_MODE) {
16841758
// If we're rendering the html tag and we're at the root (i.e. not in foreignObject)
16851759
// then we also emit the DOCTYPE as part of the root content as a convenience for
16861760
// rendering the whole document.
16871761
target.push(DOCTYPE);
16881762
}
1689-
return pushStartGenericElement(target, props, tag, responseState);
1763+
return pushStartGenericElement(target, props, 'html', responseState);
16901764
}
16911765

16921766
function pushScript(
@@ -1764,6 +1838,25 @@ function pushScriptImpl(
17641838
return null;
17651839
}
17661840

1841+
function pushHtmlEmbedding(
1842+
preamble: Array<Chunk | PrecomputedChunk>,
1843+
postamble: Array<Chunk | PrecomputedChunk>,
1844+
responseState: ResponseState,
1845+
): void {
1846+
responseState.hasHtml = true;
1847+
preamble.push(DOCTYPE);
1848+
preamble.push(startChunkForTag('html'), endOfStartTag);
1849+
postamble.push(endTag1, stringToChunk('html'), endTag2);
1850+
}
1851+
1852+
function pushBodyEmbedding(
1853+
target: Array<Chunk | PrecomputedChunk>,
1854+
postamble: Array<Chunk | PrecomputedChunk>,
1855+
): void {
1856+
target.push(startChunkForTag('body'), endOfStartTag);
1857+
postamble.push(endTag1, stringToChunk('body'), endTag2);
1858+
}
1859+
17671860
function pushStartGenericElement(
17681861
target: Array<Chunk | PrecomputedChunk>,
17691862
props: Object,
@@ -1981,6 +2074,7 @@ const DOCTYPE: PrecomputedChunk = stringToPrecomputedChunk('<!DOCTYPE html>');
19812074
export function pushStartInstance(
19822075
target: Array<Chunk | PrecomputedChunk>,
19832076
preamble: Array<Chunk | PrecomputedChunk>,
2077+
postamble: Array<Chunk | PrecomputedChunk>,
19842078
type: string,
19852079
props: Object,
19862080
responseState: ResponseState,
@@ -2024,6 +2118,31 @@ export function pushStartInstance(
20242118
}
20252119
}
20262120

2121+
if (enableFloat) {
2122+
if (responseState.requiresEmbedding) {
2123+
responseState.requiresEmbedding = false;
2124+
if (__DEV__) {
2125+
// Dev only marker for later
2126+
(responseState: any).isDocumentEmbedded = true;
2127+
}
2128+
switch (type) {
2129+
case 'html': {
2130+
// noop
2131+
break;
2132+
}
2133+
case 'head':
2134+
case 'body': {
2135+
pushHtmlEmbedding(preamble, postamble, responseState);
2136+
break;
2137+
}
2138+
default: {
2139+
pushBodyEmbedding(target, postamble);
2140+
pushHtmlEmbedding(preamble, postamble, responseState);
2141+
}
2142+
}
2143+
}
2144+
}
2145+
20272146
switch (type) {
20282147
// Special tags
20292148
case 'select':
@@ -2113,13 +2232,12 @@ export function pushStartInstance(
21132232
}
21142233
// Preamble start tags
21152234
case 'head':
2116-
return pushStartHead(target, preamble, props, type, responseState);
2235+
return pushStartHead(target, preamble, props, responseState);
21172236
case 'html': {
21182237
return pushStartHtml(
21192238
target,
21202239
preamble,
21212240
props,
2122-
type,
21232241
responseState,
21242242
formatContext,
21252243
);
@@ -2195,6 +2313,35 @@ export function pushEndInstance(
21952313
target.push(endTag1, stringToChunk(type), endTag2);
21962314
}
21972315

2316+
export function writePreambleOpen(
2317+
destination: Destination,
2318+
preamble: Array<Chunk | PrecomputedChunk>,
2319+
responseState: ResponseState,
2320+
): void {
2321+
for (let i = 0; i < preamble.length; i++) {
2322+
writeChunk(destination, preamble[i]);
2323+
}
2324+
preamble.length = 0;
2325+
if (enableFloat) {
2326+
if (responseState.hasHtml && !responseState.hasHead) {
2327+
responseState.hasHead = true;
2328+
writeChunk(destination, startChunkForTag('head'));
2329+
writeChunk(destination, endOfStartTag);
2330+
preamble.push(endTag1, stringToChunk('head'), endTag2);
2331+
}
2332+
}
2333+
}
2334+
2335+
export function writePreambleClose(
2336+
destination: Destination,
2337+
preamble: Array<Chunk | PrecomputedChunk>,
2338+
): void {
2339+
for (let i = 0; i < preamble.length; i++) {
2340+
writeChunk(destination, preamble[i]);
2341+
}
2342+
preamble.length = 0;
2343+
}
2344+
21982345
export function writeCompletedRoot(
21992346
destination: Destination,
22002347
responseState: ResponseState,

packages/react-dom-bindings/src/server/ReactDOMServerLegacyFormatConfig.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ export type ResponseState = {
3737
// Keep this in sync with ReactDOMServerFormatConfig
3838
bootstrapChunks: Array<Chunk | PrecomputedChunk>,
3939
fallbackBootstrapChunks: void | Array<Chunk | PrecomputedChunk>,
40+
requiresEmbedding: boolean,
41+
hasHead: boolean,
42+
hasHtml: boolean,
4043
placeholderPrefix: PrecomputedChunk,
4144
segmentPrefix: PrecomputedChunk,
4245
boundaryPrefix: string,
@@ -75,6 +78,9 @@ export function createResponseState(
7578
// Keep this in sync with ReactDOMServerFormatConfig
7679
bootstrapChunks: responseState.bootstrapChunks,
7780
fallbackBootstrapChunks: responseState.fallbackBootstrapChunks,
81+
requiresEmbedding: false,
82+
hasHead: false,
83+
hasHtml: false,
7884
placeholderPrefix: responseState.placeholderPrefix,
7985
segmentPrefix: responseState.segmentPrefix,
8086
boundaryPrefix: responseState.boundaryPrefix,
@@ -137,6 +143,8 @@ export {
137143
prepareToRender,
138144
cleanupAfterRender,
139145
getRootBoundaryID,
146+
writePreambleOpen,
147+
writePreambleClose,
140148
} from './ReactDOMServerFormatConfig';
141149

142150
import {stringToChunk} from 'react-server/src/ReactServerStreamConfig';

packages/react-dom/npm/server.browser.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,6 @@ exports.renderToReadableStream = s.renderToReadableStream;
1919
if (typeof s.renderIntoContainer === 'function') {
2020
exports.renderIntoContainer = s.renderIntoContainer;
2121
}
22+
if (typeof s.renderIntoDocument === 'function') {
23+
exports.renderIntoDocument = s.renderIntoDocument;
24+
}

packages/react-dom/npm/server.bun.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,6 @@ exports.renderToReadableStream = s.renderToReadableStream;
1919
if (typeof s.renderIntoContainer === 'function') {
2020
exports.renderIntoContainer = s.renderIntoContainer;
2121
}
22+
if (typeof s.renderIntoDocument === 'function') {
23+
exports.renderIntoDocument = s.renderIntoDocument;
24+
}

packages/react-dom/npm/server.node.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,7 @@ if (typeof s.renderIntoContainerAsPipeableStream === 'function') {
2020
exports.renderIntoContainerAsPipeableStream =
2121
s.renderIntoContainerAsPipeableStream;
2222
}
23+
if (typeof s.renderIntoDocumentAsPipeableStream === 'function') {
24+
exports.renderIntoDocumentAsPipeableStream =
25+
s.renderIntoDocumentAsPipeableStream;
26+
}

packages/react-dom/server.browser.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,10 @@ export function renderIntoContainer() {
4949
arguments,
5050
);
5151
}
52+
53+
export function renderIntoDocument() {
54+
return require('./src/server/ReactDOMFizzServerBrowser').renderIntoDocument.apply(
55+
this,
56+
arguments,
57+
);
58+
}

packages/react-dom/server.bun.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,17 @@ export function renderToReadableStream() {
4545
arguments,
4646
);
4747
}
48+
4849
export function renderIntoContainer() {
4950
return require('./src/server/ReactDOMFizzServerBun').renderIntoContainer.apply(
5051
this,
5152
arguments,
5253
);
5354
}
55+
56+
export function renderIntoDocument() {
57+
return require('./src/server/ReactDOMFizzServerBun').renderIntoDocument.apply(
58+
this,
59+
arguments,
60+
);
61+
}

packages/react-dom/server.node.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,10 @@ export function renderIntoContainerAsPipeableStream() {
4949
arguments,
5050
);
5151
}
52+
53+
export function renderIntoDocumentAsPipeableStream() {
54+
return require('./src/server/ReactDOMFizzServerNode').renderIntoDocumentAsPipeableStream.apply(
55+
this,
56+
arguments,
57+
);
58+
}

0 commit comments

Comments
 (0)