mirror of
https://github.com/zebrajr/react.git
synced 2025-12-06 12:20:20 +01:00
The semantics of React is that anything outside of Suspense boundaries in a transition doesn't display until it has fully unsuspended. With SSR streaming the intention is to preserve that. We explicitly don't want to support the mode of document streaming normally supported by the browser where it can paint content as tags stream in since that leads to content popping in and thrashing in unpredictable ways. This should instead be modeled explictly by nested Suspense boundaries or something like SuspenseList. After the first shell any nested Suspense boundaries are only revealed, by script, once they're fully streamed in to the next boundary. So this is already the case there. However, for the initial shell we have been at the mercy of browser heuristics for how long it decides to stream before the first paint. Chromium now has [an API explicitly for this use case](https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API/Using#stabilizing_page_state_to_make_cross-document_transitions_consistent) that lets us model the semantics that we want. This is always important but especially so with MPA View Transitions. After this a simple document looks like this: ```html <!DOCTYPE html> <html> <head> <link rel="expect" href="#«R»" blocking="render"/> </head> <body> <p>hello world</p> <script src="bootstrap.js" id="«R»" async=""></script> ... </body> </html> ``` The `rel="expect"` tag indicates that we want to wait to paint until we have streamed far enough to be able to paint the id `"«R»"` which indicates the shell. Ideally this `id` would be assigned to the root most HTML element in the body. However, this is tricky in our implementation because there can be multiple and we can render them out of order. So instead, we assign the id to the first bootstrap script if there is one since these are always added to the end of the shell. If there isn't a bootstrap script then we emit an empty `<template id="«R»"></template>` instead as a marker. Since we currently put as much as possible in the shell if it's loaded by the time we render, this can have some negative effects for very large documents. We should instead apply the heuristic where very large Suspense boundaries get outlined outside the shell even if they're immediately available. This means that even prerenders can end up with script tags. We only emit the `rel="expect"` if you're rendering a whole document. I.e. if you rendered either a `<html>` or `<head>` tag. If you're rendering a partial document, then we don't really know where the streaming parts are anyway and can't provide such guarantees. This does apply whether you're streaming or not because we still want to block rendering until the end, but in practice any serialized state that needs hydrate should still be embedded after the completion id.
234 lines
5.7 KiB
JavaScript
234 lines
5.7 KiB
JavaScript
/**
|
|
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow
|
|
*/
|
|
|
|
import type {ReactNodeList} from 'shared/ReactTypes';
|
|
|
|
import type {
|
|
RenderState,
|
|
ResumableState,
|
|
PreambleState,
|
|
HoistableState,
|
|
FormatContext,
|
|
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
|
|
|
|
import {
|
|
pushStartInstance as pushStartInstanceImpl,
|
|
writePreambleStart as writePreambleStartImpl,
|
|
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
|
|
|
|
import type {
|
|
Destination,
|
|
Chunk,
|
|
PrecomputedChunk,
|
|
} from 'react-server/src/ReactServerStreamConfig';
|
|
|
|
import type {FormStatus} from 'react-dom-bindings/src/shared/ReactDOMFormActions';
|
|
|
|
import {NotPending} from 'react-dom-bindings/src/shared/ReactDOMFormActions';
|
|
|
|
import hasOwnProperty from 'shared/hasOwnProperty';
|
|
|
|
// Allow embedding inside another Fizz render.
|
|
export const isPrimaryRenderer = false;
|
|
|
|
// Disable Client Hooks
|
|
export const supportsClientAPIs = false;
|
|
|
|
import {stringToChunk} from 'react-server/src/ReactServerStreamConfig';
|
|
|
|
export type {
|
|
RenderState,
|
|
ResumableState,
|
|
HoistableState,
|
|
PreambleState,
|
|
FormatContext,
|
|
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
|
|
|
|
export {
|
|
getChildFormatContext,
|
|
makeId,
|
|
pushEndInstance,
|
|
pushFormStateMarkerIsMatching,
|
|
pushFormStateMarkerIsNotMatching,
|
|
writeStartSegment,
|
|
writeEndSegment,
|
|
writeCompletedSegmentInstruction,
|
|
writeCompletedBoundaryInstruction,
|
|
writeClientRenderBoundaryInstruction,
|
|
writeStartPendingSuspenseBoundary,
|
|
writeEndPendingSuspenseBoundary,
|
|
writeHoistablesForBoundary,
|
|
writePlaceholder,
|
|
createRootFormatContext,
|
|
createRenderState,
|
|
createResumableState,
|
|
createPreambleState,
|
|
createHoistableState,
|
|
writePreambleEnd,
|
|
writeHoistables,
|
|
writePostamble,
|
|
hoistHoistables,
|
|
resetResumableState,
|
|
completeResumableState,
|
|
emitEarlyPreloads,
|
|
doctypeChunk,
|
|
canHavePreamble,
|
|
hoistPreambleState,
|
|
isPreambleReady,
|
|
isPreambleContext,
|
|
} from 'react-dom-bindings/src/server/ReactFizzConfigDOM';
|
|
|
|
import escapeTextForBrowser from 'react-dom-bindings/src/server/escapeTextForBrowser';
|
|
|
|
export function pushStartInstance(
|
|
target: Array<Chunk | PrecomputedChunk>,
|
|
type: string,
|
|
props: Object,
|
|
resumableState: ResumableState,
|
|
renderState: RenderState,
|
|
preambleState: null | PreambleState,
|
|
hoistableState: null | HoistableState,
|
|
formatContext: FormatContext,
|
|
textEmbedded: boolean,
|
|
isFallback: boolean,
|
|
): ReactNodeList {
|
|
for (const propKey in props) {
|
|
if (hasOwnProperty.call(props, propKey)) {
|
|
const propValue = props[propKey];
|
|
if (propKey === 'ref' && propValue != null) {
|
|
throw new Error(
|
|
'Cannot pass ref in renderToHTML because they will never be hydrated.',
|
|
);
|
|
}
|
|
if (typeof propValue === 'function') {
|
|
throw new Error(
|
|
'Cannot pass event handlers (' +
|
|
propKey +
|
|
') in renderToHTML because ' +
|
|
'the HTML will never be hydrated so they can never get called.',
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return pushStartInstanceImpl(
|
|
target,
|
|
type,
|
|
props,
|
|
resumableState,
|
|
renderState,
|
|
preambleState,
|
|
hoistableState,
|
|
formatContext,
|
|
textEmbedded,
|
|
isFallback,
|
|
);
|
|
}
|
|
|
|
export function pushTextInstance(
|
|
target: Array<Chunk | PrecomputedChunk>,
|
|
text: string,
|
|
renderState: RenderState,
|
|
textEmbedded: boolean,
|
|
): boolean {
|
|
// Markup doesn't need any termination.
|
|
target.push(stringToChunk(escapeTextForBrowser(text)));
|
|
return false;
|
|
}
|
|
|
|
export function pushSegmentFinale(
|
|
target: Array<Chunk | PrecomputedChunk>,
|
|
renderState: RenderState,
|
|
lastPushedText: boolean,
|
|
textEmbedded: boolean,
|
|
): void {
|
|
// Markup doesn't need any termination.
|
|
return;
|
|
}
|
|
|
|
export function pushStartActivityBoundary(
|
|
target: Array<Chunk | PrecomputedChunk>,
|
|
renderState: RenderState,
|
|
): void {
|
|
// Markup doesn't have any instructions.
|
|
return;
|
|
}
|
|
|
|
export function pushEndActivityBoundary(
|
|
target: Array<Chunk | PrecomputedChunk>,
|
|
renderState: RenderState,
|
|
): void {
|
|
// Markup doesn't have any instructions.
|
|
return;
|
|
}
|
|
|
|
export function writeStartCompletedSuspenseBoundary(
|
|
destination: Destination,
|
|
renderState: RenderState,
|
|
): boolean {
|
|
// Markup doesn't have any instructions.
|
|
return true;
|
|
}
|
|
|
|
export function writeStartClientRenderedSuspenseBoundary(
|
|
destination: Destination,
|
|
renderState: RenderState,
|
|
// flushing these error arguments are not currently supported in this legacy streaming format.
|
|
errorDigest: ?string,
|
|
errorMessage: ?string,
|
|
errorStack: ?string,
|
|
errorComponentStack: ?string,
|
|
): boolean {
|
|
// Markup doesn't have any instructions.
|
|
return true;
|
|
}
|
|
|
|
export function writeEndCompletedSuspenseBoundary(
|
|
destination: Destination,
|
|
renderState: RenderState,
|
|
): boolean {
|
|
// Markup doesn't have any instructions.
|
|
return true;
|
|
}
|
|
export function writeEndClientRenderedSuspenseBoundary(
|
|
destination: Destination,
|
|
renderState: RenderState,
|
|
): boolean {
|
|
// Markup doesn't have any instructions.
|
|
return true;
|
|
}
|
|
|
|
export function writePreambleStart(
|
|
destination: Destination,
|
|
resumableState: ResumableState,
|
|
renderState: RenderState,
|
|
willFlushAllSegments: boolean,
|
|
skipExpect?: boolean, // Used as an override by ReactFizzConfigMarkup
|
|
): void {
|
|
return writePreambleStartImpl(
|
|
destination,
|
|
resumableState,
|
|
renderState,
|
|
willFlushAllSegments,
|
|
true, // skipExpect
|
|
);
|
|
}
|
|
|
|
export function writeCompletedRoot(
|
|
destination: Destination,
|
|
resumableState: ResumableState,
|
|
renderState: RenderState,
|
|
): boolean {
|
|
// Markup doesn't have any bootstrap scripts nor shell completions.
|
|
return true;
|
|
}
|
|
|
|
export type TransitionStatus = FormStatus;
|
|
export const NotPendingTransition: TransitionStatus = NotPending;
|