Skip to content

Commit bb08b6d

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 688be00 commit bb08b6d

23 files changed

+604
-22
lines changed

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

Lines changed: 158 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,9 @@ const DataStreamingFormat: StreamingFormat = 1;
119119
export type ResponseState = {
120120
bootstrapChunks: Array<Chunk | PrecomputedChunk>,
121121
fallbackBootstrapChunks: void | Array<Chunk | PrecomputedChunk>,
122+
requiresEmbedding: boolean,
123+
hasHead: boolean,
124+
hasHtml: boolean,
122125
placeholderPrefix: PrecomputedChunk,
123126
segmentPrefix: PrecomputedChunk,
124127
boundaryPrefix: string,
@@ -191,6 +194,7 @@ export function createResponseState(
191194
> | void,
192195
externalRuntimeConfig: string | BootstrapScriptDescriptor | void,
193196
containerID: string | void,
197+
documentEmbedding: boolean | void,
194198
): ResponseState {
195199
const idPrefix = identifierPrefix === undefined ? '' : identifierPrefix;
196200
const inlineScriptWithNonce =
@@ -327,6 +331,9 @@ export function createResponseState(
327331
fallbackBootstrapChunks: fallbackBootstrapChunks.length
328332
? fallbackBootstrapChunks
329333
: undefined,
334+
requiresEmbedding: documentEmbedding === true,
335+
hasHead: false,
336+
hasHtml: false,
330337
placeholderPrefix: stringToPrecomputedChunk(idPrefix + 'P:'),
331338
segmentPrefix: stringToPrecomputedChunk(idPrefix + 'S:'),
332339
boundaryPrefix: idPrefix + 'B:',
@@ -1652,33 +1659,100 @@ function pushStartHead(
16521659
target: Array<Chunk | PrecomputedChunk>,
16531660
preamble: Array<Chunk | PrecomputedChunk>,
16541661
props: Object,
1655-
tag: string,
16561662
responseState: ResponseState,
16571663
): ReactNodeList {
1658-
return pushStartGenericElement(
1659-
enableFloat ? preamble : target,
1660-
props,
1661-
tag,
1662-
responseState,
1663-
);
1664+
if (enableFloat) {
1665+
let children = null;
1666+
let innerHTML = null;
1667+
let includedAttributeProps = false;
1668+
1669+
if (!responseState.hasHead) {
1670+
responseState.hasHead = true;
1671+
preamble.push(startChunkForTag('head'));
1672+
for (const propKey in props) {
1673+
if (hasOwnProperty.call(props, propKey)) {
1674+
const propValue = props[propKey];
1675+
if (propValue == null) {
1676+
continue;
1677+
}
1678+
switch (propKey) {
1679+
case 'children':
1680+
children = propValue;
1681+
break;
1682+
case 'dangerouslySetInnerHTML':
1683+
innerHTML = propValue;
1684+
break;
1685+
default:
1686+
if (__DEV__) {
1687+
includedAttributeProps = true;
1688+
}
1689+
pushAttribute(preamble, responseState, propKey, propValue);
1690+
break;
1691+
}
1692+
}
1693+
}
1694+
preamble.push(endOfStartTag);
1695+
} else {
1696+
// We elide the actual <head> tag because it was previously rendered but we still need
1697+
// to render children/innerHTML
1698+
for (const propKey in props) {
1699+
if (hasOwnProperty.call(props, propKey)) {
1700+
const propValue = props[propKey];
1701+
if (propValue == null) {
1702+
continue;
1703+
}
1704+
switch (propKey) {
1705+
case 'children':
1706+
children = propValue;
1707+
break;
1708+
case 'dangerouslySetInnerHTML':
1709+
innerHTML = propValue;
1710+
break;
1711+
default:
1712+
if (__DEV__) {
1713+
includedAttributeProps = true;
1714+
}
1715+
break;
1716+
}
1717+
}
1718+
}
1719+
}
1720+
1721+
if (__DEV__) {
1722+
if ((responseState: any).isDocumentEmbedded && includedAttributeProps) {
1723+
// We use this embedded flag a heuristic for whether we are rendering with renderIntoDocument
1724+
console.error(
1725+
'A <head> tag was rendered with props when using "renderIntoDocument". In this rendering mode' +
1726+
' React may emit the head tag early in some circumstances and therefore props on the <head> tag are not' +
1727+
' supported and may be missing in the rendered output for any particular render. In many cases props that' +
1728+
' are set on a <head> tag can be set on the <html> tag instead.',
1729+
);
1730+
}
1731+
}
1732+
1733+
pushInnerHTML(target, innerHTML, children);
1734+
return children;
1735+
} else {
1736+
return pushStartGenericElement(target, props, 'head', responseState);
1737+
}
16641738
}
16651739

16661740
function pushStartHtml(
16671741
target: Array<Chunk | PrecomputedChunk>,
16681742
preamble: Array<Chunk | PrecomputedChunk>,
16691743
props: Object,
1670-
tag: string,
16711744
responseState: ResponseState,
16721745
formatContext: FormatContext,
16731746
): ReactNodeList {
1747+
responseState.hasHtml = true;
16741748
target = enableFloat ? preamble : target;
16751749
if (formatContext.insertionMode === ROOT_HTML_MODE) {
16761750
// If we're rendering the html tag and we're at the root (i.e. not in foreignObject)
16771751
// then we also emit the DOCTYPE as part of the root content as a convenience for
16781752
// rendering the whole document.
16791753
target.push(DOCTYPE);
16801754
}
1681-
return pushStartGenericElement(target, props, tag, responseState);
1755+
return pushStartGenericElement(target, props, 'html', responseState);
16821756
}
16831757

16841758
function pushScript(
@@ -1756,6 +1830,25 @@ function pushScriptImpl(
17561830
return null;
17571831
}
17581832

1833+
function pushHtmlEmbedding(
1834+
preamble: Array<Chunk | PrecomputedChunk>,
1835+
postamble: Array<Chunk | PrecomputedChunk>,
1836+
responseState: ResponseState,
1837+
): void {
1838+
responseState.hasHtml = true;
1839+
preamble.push(DOCTYPE);
1840+
preamble.push(startChunkForTag('html'), endOfStartTag);
1841+
postamble.push(endTag1, stringToChunk('html'), endTag2);
1842+
}
1843+
1844+
function pushBodyEmbedding(
1845+
target: Array<Chunk | PrecomputedChunk>,
1846+
postamble: Array<Chunk | PrecomputedChunk>,
1847+
): void {
1848+
target.push(startChunkForTag('body'), endOfStartTag);
1849+
postamble.push(endTag1, stringToChunk('body'), endTag2);
1850+
}
1851+
17591852
function pushStartGenericElement(
17601853
target: Array<Chunk | PrecomputedChunk>,
17611854
props: Object,
@@ -1973,6 +2066,7 @@ const DOCTYPE: PrecomputedChunk = stringToPrecomputedChunk('<!DOCTYPE html>');
19732066
export function pushStartInstance(
19742067
target: Array<Chunk | PrecomputedChunk>,
19752068
preamble: Array<Chunk | PrecomputedChunk>,
2069+
postamble: Array<Chunk | PrecomputedChunk>,
19762070
type: string,
19772071
props: Object,
19782072
responseState: ResponseState,
@@ -2016,6 +2110,31 @@ export function pushStartInstance(
20162110
}
20172111
}
20182112

2113+
if (enableFloat) {
2114+
if (responseState.requiresEmbedding) {
2115+
responseState.requiresEmbedding = false;
2116+
if (__DEV__) {
2117+
// Dev only marker for later
2118+
(responseState: any).isDocumentEmbedded = true;
2119+
}
2120+
switch (type) {
2121+
case 'html': {
2122+
// noop
2123+
break;
2124+
}
2125+
case 'head':
2126+
case 'body': {
2127+
pushHtmlEmbedding(preamble, postamble, responseState);
2128+
break;
2129+
}
2130+
default: {
2131+
pushBodyEmbedding(target, postamble);
2132+
pushHtmlEmbedding(preamble, postamble, responseState);
2133+
}
2134+
}
2135+
}
2136+
}
2137+
20192138
switch (type) {
20202139
// Special tags
20212140
case 'select':
@@ -2105,13 +2224,12 @@ export function pushStartInstance(
21052224
}
21062225
// Preamble start tags
21072226
case 'head':
2108-
return pushStartHead(target, preamble, props, type, responseState);
2227+
return pushStartHead(target, preamble, props, responseState);
21092228
case 'html': {
21102229
return pushStartHtml(
21112230
target,
21122231
preamble,
21132232
props,
2114-
type,
21152233
responseState,
21162234
formatContext,
21172235
);
@@ -2187,6 +2305,35 @@ export function pushEndInstance(
21872305
target.push(endTag1, stringToChunk(type), endTag2);
21882306
}
21892307

2308+
export function writePreambleOpen(
2309+
destination: Destination,
2310+
preamble: Array<Chunk | PrecomputedChunk>,
2311+
responseState: ResponseState,
2312+
): void {
2313+
for (let i = 0; i < preamble.length; i++) {
2314+
writeChunk(destination, preamble[i]);
2315+
}
2316+
preamble.length = 0;
2317+
if (enableFloat) {
2318+
if (responseState.hasHtml && !responseState.hasHead) {
2319+
responseState.hasHead = true;
2320+
writeChunk(destination, startChunkForTag('head'));
2321+
writeChunk(destination, endOfStartTag);
2322+
preamble.push(endTag1, stringToChunk('head'), endTag2);
2323+
}
2324+
}
2325+
}
2326+
2327+
export function writePreambleClose(
2328+
destination: Destination,
2329+
preamble: Array<Chunk | PrecomputedChunk>,
2330+
): void {
2331+
for (let i = 0; i < preamble.length; i++) {
2332+
writeChunk(destination, preamble[i]);
2333+
}
2334+
preamble.length = 0;
2335+
}
2336+
21902337
export function writeCompletedRoot(
21912338
destination: Destination,
21922339
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/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)