Skip to content

Commit

Permalink
[Float][Fizz][Legacy] hoisted elements no longer emit before <html>
Browse files Browse the repository at this point in the history
… in legacy apis such as `renderToString()` (facebook#27269)

renderToString is a legacy server API which used a trick to avoid having
the DOCTYPE included when rendering full documents by setting the root
formatcontext to HTML_MODE rather than ROOT_HTML_MODE. Previously this
was of little consequence but with Float the Root mode started to be
used for things like determining if we could flush hoistable elements
yet. In issue facebook#27177 we see that hoisted elements can appear before the
<html> tag when using a legacy API `renderToString`.

This change exports a DOCTYPE from FizzConfigDOM and FizzConfigDOMLegacy
respectively, using an empty chunk in the legacy case. The only runtime
perf cost here is that for legacy APIs there is an extra empty chunk to
write when rendering a top level <html> tag which is trivial enough

Fixes facebook#27177
  • Loading branch information
gnoff authored and AndyPengc12 committed Apr 15, 2024
1 parent 034ecbe commit df83069
Show file tree
Hide file tree
Showing 3 changed files with 55 additions and 14 deletions.
9 changes: 6 additions & 3 deletions packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js
Original file line number Diff line number Diff line change
Expand Up @@ -371,11 +371,11 @@ export function createResponseState(
// Constants for the insertion mode we're currently writing in. We don't encode all HTML5 insertion
// modes. We only include the variants as they matter for the sake of our purposes.
// We don't actually provide the namespace therefore we use constants instead of the string.
const ROOT_HTML_MODE = 0; // Used for the root most element tag.
export const ROOT_HTML_MODE = 0; // Used for the root most element tag.
// We have a less than HTML_HTML_MODE check elsewhere. If you add more cases here, make sure it
// still makes sense
const HTML_HTML_MODE = 1; // Used for the <html> if it is at the top level.
export const HTML_MODE = 2;
const HTML_MODE = 2;
const SVG_MODE = 3;
const MATHML_MODE = 4;
const HTML_TABLE_MODE = 5;
Expand Down Expand Up @@ -3027,7 +3027,10 @@ function startChunkForTag(tag: string): PrecomputedChunk {
return tagStartChunk;
}

const DOCTYPE: PrecomputedChunk = stringToPrecomputedChunk('<!DOCTYPE html>');
export const doctypeChunk: PrecomputedChunk =
stringToPrecomputedChunk('<!DOCTYPE html>');

import {doctypeChunk as DOCTYPE} from 'react-server/src/ReactFizzConfig';

export function pushStartInstance(
target: Array<Chunk | PrecomputedChunk>,
Expand Down
19 changes: 8 additions & 11 deletions packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import type {
Resources,
BootstrapScriptDescriptor,
ExternalRuntimeScript,
FormatContext,
StreamingFormat,
InstructionState,
} from './ReactFizzConfigDOM';
Expand All @@ -24,7 +23,6 @@ import {
writeStartClientRenderedSuspenseBoundary as writeStartClientRenderedSuspenseBoundaryImpl,
writeEndCompletedSuspenseBoundary as writeEndCompletedSuspenseBoundaryImpl,
writeEndClientRenderedSuspenseBoundary as writeEndClientRenderedSuspenseBoundaryImpl,
HTML_MODE,
} from './ReactFizzConfigDOM';

import type {
Expand Down Expand Up @@ -104,13 +102,13 @@ export function createResponseState(
};
}

export function createRootFormatContext(): FormatContext {
return {
insertionMode: HTML_MODE, // We skip the root mode because we don't want to emit the DOCTYPE in legacy mode.
selectedValue: null,
noscriptTagInScope: false,
};
}
import {
stringToChunk,
stringToPrecomputedChunk,
} from 'react-server/src/ReactServerStreamConfig';

// this chunk is empty on purpose because we do not want to emit the DOCTYPE in legacy mode
export const doctypeChunk: PrecomputedChunk = stringToPrecomputedChunk('');

export type {
Resources,
Expand Down Expand Up @@ -138,6 +136,7 @@ export {
writeResourcesForBoundary,
writePlaceholder,
writeCompletedRoot,
createRootFormatContext,
createResources,
createBoundaryResources,
writePreamble,
Expand All @@ -148,8 +147,6 @@ export {
prepareHostDispatcher,
} from './ReactFizzConfigDOM';

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

import escapeTextForBrowser from './escapeTextForBrowser';

export function pushTextInstance(
Expand Down
41 changes: 41 additions & 0 deletions packages/react-dom/src/__tests__/ReactDOMLegacyFloat-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* 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.
*
* @emails react-core
* @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment
*/

'use strict';

let React;
let ReactDOMFizzServer;

describe('ReactDOMFloat', () => {
beforeEach(() => {
jest.resetModules();

React = require('react');
ReactDOMFizzServer = require('react-dom/server');
});

// fixes #27177
// @gate enableFloat
it('does not hoist above the <html> tag', async () => {
const result = ReactDOMFizzServer.renderToString(
<html>
<head>
<script src="foo" />
<meta charSet="utf-8" />
<title>title</title>
</head>
</html>,
);

expect(result).toEqual(
'<html><head><meta charSet="utf-8"/><title>title</title><script src="foo"></script></head></html>',
);
});
});

0 comments on commit df83069

Please sign in to comment.