Skip to content

Commit

Permalink
Implement renderIntoDocument
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
gnoff committed Jan 16, 2023
1 parent 8be28e5 commit fc2b823
Show file tree
Hide file tree
Showing 35 changed files with 646 additions and 24 deletions.
169 changes: 158 additions & 11 deletions packages/react-dom-bindings/src/server/ReactDOMServerFormatConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ const DataStreamingFormat: StreamingFormat = 1;
export type ResponseState = {
bootstrapChunks: Array<Chunk | PrecomputedChunk>,
fallbackBootstrapChunks: void | Array<Chunk | PrecomputedChunk>,
requiresEmbedding: boolean,
hasHead: boolean,
hasHtml: boolean,
placeholderPrefix: PrecomputedChunk,
segmentPrefix: PrecomputedChunk,
boundaryPrefix: string,
Expand Down Expand Up @@ -199,6 +202,7 @@ export function createResponseState(
> | void,
externalRuntimeConfig: string | BootstrapScriptDescriptor | void,
containerID: string | void,
documentEmbedding: boolean | void,
): ResponseState {
const idPrefix = identifierPrefix === undefined ? '' : identifierPrefix;
const inlineScriptWithNonce =
Expand Down Expand Up @@ -335,6 +339,9 @@ export function createResponseState(
fallbackBootstrapChunks: fallbackBootstrapChunks.length
? fallbackBootstrapChunks
: undefined,
requiresEmbedding: documentEmbedding === true,
hasHead: false,
hasHtml: false,
placeholderPrefix: stringToPrecomputedChunk(idPrefix + 'P:'),
segmentPrefix: stringToPrecomputedChunk(idPrefix + 'S:'),
boundaryPrefix: idPrefix + 'B:',
Expand Down Expand Up @@ -1660,33 +1667,100 @@ function pushStartHead(
target: Array<Chunk | PrecomputedChunk>,
preamble: Array<Chunk | PrecomputedChunk>,
props: Object,
tag: string,
responseState: ResponseState,
): ReactNodeList {
return pushStartGenericElement(
enableFloat ? preamble : target,
props,
tag,
responseState,
);
if (enableFloat) {
let children = null;
let innerHTML = null;
let includedAttributeProps = false;

if (!responseState.hasHead) {
responseState.hasHead = true;
preamble.push(startChunkForTag('head'));
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:
if (__DEV__) {
includedAttributeProps = true;
}
pushAttribute(preamble, responseState, propKey, propValue);
break;
}
}
}
preamble.push(endOfStartTag);
} else {
// We elide the actual <head> tag because it was previously rendered but we still need
// to render children/innerHTML
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:
if (__DEV__) {
includedAttributeProps = true;
}
break;
}
}
}
}

if (__DEV__) {
if ((responseState: any).isDocumentEmbedded && includedAttributeProps) {
// We use this embedded flag a heuristic for whether we are rendering with renderIntoDocument
console.error(
'A <head> tag was rendered with props when using "renderIntoDocument". In this rendering mode' +
' React may emit the head tag early in some circumstances and therefore props on the <head> tag are not' +
' supported and may be missing in the rendered output for any particular render. In many cases props that' +
' are set on a <head> tag can be set on the <html> tag instead.',
);
}
}

pushInnerHTML(target, innerHTML, children);
return children;
} else {
return pushStartGenericElement(target, props, 'head', responseState);
}
}

function pushStartHtml(
target: Array<Chunk | PrecomputedChunk>,
preamble: Array<Chunk | PrecomputedChunk>,
props: Object,
tag: string,
responseState: ResponseState,
formatContext: FormatContext,
): ReactNodeList {
responseState.hasHtml = true;
target = enableFloat ? preamble : target;
if (formatContext.insertionMode === ROOT_HTML_MODE) {
// If we're rendering the html tag and we're at the root (i.e. not in foreignObject)
// then we also emit the DOCTYPE as part of the root content as a convenience for
// rendering the whole document.
target.push(DOCTYPE);
}
return pushStartGenericElement(target, props, tag, responseState);
return pushStartGenericElement(target, props, 'html', responseState);
}

function pushScript(
Expand Down Expand Up @@ -1764,6 +1838,25 @@ function pushScriptImpl(
return null;
}

function pushHtmlEmbedding(
preamble: Array<Chunk | PrecomputedChunk>,
postamble: Array<Chunk | PrecomputedChunk>,
responseState: ResponseState,
): void {
responseState.hasHtml = true;
preamble.push(DOCTYPE);
preamble.push(startChunkForTag('html'), endOfStartTag);
postamble.push(endTag1, stringToChunk('html'), endTag2);
}

function pushBodyEmbedding(
target: Array<Chunk | PrecomputedChunk>,
postamble: Array<Chunk | PrecomputedChunk>,
): void {
target.push(startChunkForTag('body'), endOfStartTag);
postamble.push(endTag1, stringToChunk('body'), endTag2);
}

function pushStartGenericElement(
target: Array<Chunk | PrecomputedChunk>,
props: Object,
Expand Down Expand Up @@ -1981,6 +2074,7 @@ const DOCTYPE: PrecomputedChunk = stringToPrecomputedChunk('<!DOCTYPE html>');
export function pushStartInstance(
target: Array<Chunk | PrecomputedChunk>,
preamble: Array<Chunk | PrecomputedChunk>,
postamble: Array<Chunk | PrecomputedChunk>,
type: string,
props: Object,
responseState: ResponseState,
Expand Down Expand Up @@ -2024,6 +2118,31 @@ export function pushStartInstance(
}
}

if (enableFloat) {
if (responseState.requiresEmbedding) {
responseState.requiresEmbedding = false;
if (__DEV__) {
// Dev only marker for later
(responseState: any).isDocumentEmbedded = true;
}
switch (type) {
case 'html': {
// noop
break;
}
case 'head':
case 'body': {
pushHtmlEmbedding(preamble, postamble, responseState);
break;
}
default: {
pushBodyEmbedding(target, postamble);
pushHtmlEmbedding(preamble, postamble, responseState);
}
}
}
}

switch (type) {
// Special tags
case 'select':
Expand Down Expand Up @@ -2113,13 +2232,12 @@ export function pushStartInstance(
}
// Preamble start tags
case 'head':
return pushStartHead(target, preamble, props, type, responseState);
return pushStartHead(target, preamble, props, responseState);
case 'html': {
return pushStartHtml(
target,
preamble,
props,
type,
responseState,
formatContext,
);
Expand Down Expand Up @@ -2195,6 +2313,35 @@ export function pushEndInstance(
target.push(endTag1, stringToChunk(type), endTag2);
}

export function writePreambleOpen(
destination: Destination,
preamble: Array<Chunk | PrecomputedChunk>,
responseState: ResponseState,
): void {
for (let i = 0; i < preamble.length; i++) {
writeChunk(destination, preamble[i]);
}
preamble.length = 0;
if (enableFloat) {
if (responseState.hasHtml && !responseState.hasHead) {
responseState.hasHead = true;
writeChunk(destination, startChunkForTag('head'));
writeChunk(destination, endOfStartTag);
preamble.push(endTag1, stringToChunk('head'), endTag2);
}
}
}

export function writePreambleClose(
destination: Destination,
preamble: Array<Chunk | PrecomputedChunk>,
): void {
for (let i = 0; i < preamble.length; i++) {
writeChunk(destination, preamble[i]);
}
preamble.length = 0;
}

export function writeCompletedRoot(
destination: Destination,
responseState: ResponseState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ export type ResponseState = {
// Keep this in sync with ReactDOMServerFormatConfig
bootstrapChunks: Array<Chunk | PrecomputedChunk>,
fallbackBootstrapChunks: void | Array<Chunk | PrecomputedChunk>,
requiresEmbedding: boolean,
hasHead: boolean,
hasHtml: boolean,
placeholderPrefix: PrecomputedChunk,
segmentPrefix: PrecomputedChunk,
boundaryPrefix: string,
Expand Down Expand Up @@ -75,6 +78,9 @@ export function createResponseState(
// Keep this in sync with ReactDOMServerFormatConfig
bootstrapChunks: responseState.bootstrapChunks,
fallbackBootstrapChunks: responseState.fallbackBootstrapChunks,
requiresEmbedding: false,
hasHead: false,
hasHtml: false,
placeholderPrefix: responseState.placeholderPrefix,
segmentPrefix: responseState.segmentPrefix,
boundaryPrefix: responseState.boundaryPrefix,
Expand Down Expand Up @@ -137,6 +143,8 @@ export {
prepareToRender,
cleanupAfterRender,
getRootBoundaryID,
writePreambleOpen,
writePreambleClose,
} from './ReactDOMServerFormatConfig';

import {stringToChunk} from 'react-server/src/ReactServerStreamConfig';
Expand Down
3 changes: 3 additions & 0 deletions packages/react-dom/npm/server.browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ exports.renderToReadableStream = s.renderToReadableStream;
if (typeof s.renderIntoContainer === 'function') {
exports.renderIntoContainer = s.renderIntoContainer;
}
if (typeof s.renderIntoDocument === 'function') {
exports.renderIntoDocument = s.renderIntoDocument;
}
3 changes: 3 additions & 0 deletions packages/react-dom/npm/server.bun.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,6 @@ exports.renderToReadableStream = s.renderToReadableStream;
if (typeof s.renderIntoContainer === 'function') {
exports.renderIntoContainer = s.renderIntoContainer;
}
if (typeof s.renderIntoDocument === 'function') {
exports.renderIntoDocument = s.renderIntoDocument;
}
4 changes: 4 additions & 0 deletions packages/react-dom/npm/server.node.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,7 @@ if (typeof s.renderIntoContainerAsPipeableStream === 'function') {
exports.renderIntoContainerAsPipeableStream =
s.renderIntoContainerAsPipeableStream;
}
if (typeof s.renderIntoDocumentAsPipeableStream === 'function') {
exports.renderIntoDocumentAsPipeableStream =
s.renderIntoDocumentAsPipeableStream;
}
7 changes: 7 additions & 0 deletions packages/react-dom/server.browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,10 @@ export function renderIntoContainer() {
arguments,
);
}

export function renderIntoDocument() {
return require('./src/server/ReactDOMFizzServerBrowser').renderIntoDocument.apply(
this,
arguments,
);
}
8 changes: 8 additions & 0 deletions packages/react-dom/server.bun.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,17 @@ export function renderToReadableStream() {
arguments,
);
}

export function renderIntoContainer() {
return require('./src/server/ReactDOMFizzServerBun').renderIntoContainer.apply(
this,
arguments,
);
}

export function renderIntoDocument() {
return require('./src/server/ReactDOMFizzServerBun').renderIntoDocument.apply(
this,
arguments,
);
}
7 changes: 7 additions & 0 deletions packages/react-dom/server.node.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,10 @@ export function renderIntoContainerAsPipeableStream() {
arguments,
);
}

export function renderIntoDocumentAsPipeableStream() {
return require('./src/server/ReactDOMFizzServerNode').renderIntoDocumentAsPipeableStream.apply(
this,
arguments,
);
}
Loading

0 comments on commit fc2b823

Please sign in to comment.