Skip to content

Commit 86198b9

Browse files
authored
[Float][Fizz][Legacy] hoisted elements no longer emit before <html> in legacy apis such as renderToString() (#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 #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 #27177
1 parent dd480ef commit 86198b9

File tree

3 files changed

+55
-14
lines changed

3 files changed

+55
-14
lines changed

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -371,11 +371,11 @@ export function createResponseState(
371371
// Constants for the insertion mode we're currently writing in. We don't encode all HTML5 insertion
372372
// modes. We only include the variants as they matter for the sake of our purposes.
373373
// We don't actually provide the namespace therefore we use constants instead of the string.
374-
const ROOT_HTML_MODE = 0; // Used for the root most element tag.
374+
export const ROOT_HTML_MODE = 0; // Used for the root most element tag.
375375
// We have a less than HTML_HTML_MODE check elsewhere. If you add more cases here, make sure it
376376
// still makes sense
377377
const HTML_HTML_MODE = 1; // Used for the <html> if it is at the top level.
378-
export const HTML_MODE = 2;
378+
const HTML_MODE = 2;
379379
const SVG_MODE = 3;
380380
const MATHML_MODE = 4;
381381
const HTML_TABLE_MODE = 5;
@@ -3027,7 +3027,10 @@ function startChunkForTag(tag: string): PrecomputedChunk {
30273027
return tagStartChunk;
30283028
}
30293029

3030-
const DOCTYPE: PrecomputedChunk = stringToPrecomputedChunk('<!DOCTYPE html>');
3030+
export const doctypeChunk: PrecomputedChunk =
3031+
stringToPrecomputedChunk('<!DOCTYPE html>');
3032+
3033+
import {doctypeChunk as DOCTYPE} from 'react-server/src/ReactFizzConfig';
30313034

30323035
export function pushStartInstance(
30333036
target: Array<Chunk | PrecomputedChunk>,

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

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import type {
1111
Resources,
1212
BootstrapScriptDescriptor,
1313
ExternalRuntimeScript,
14-
FormatContext,
1514
StreamingFormat,
1615
InstructionState,
1716
} from './ReactFizzConfigDOM';
@@ -24,7 +23,6 @@ import {
2423
writeStartClientRenderedSuspenseBoundary as writeStartClientRenderedSuspenseBoundaryImpl,
2524
writeEndCompletedSuspenseBoundary as writeEndCompletedSuspenseBoundaryImpl,
2625
writeEndClientRenderedSuspenseBoundary as writeEndClientRenderedSuspenseBoundaryImpl,
27-
HTML_MODE,
2826
} from './ReactFizzConfigDOM';
2927

3028
import type {
@@ -104,13 +102,13 @@ export function createResponseState(
104102
};
105103
}
106104

107-
export function createRootFormatContext(): FormatContext {
108-
return {
109-
insertionMode: HTML_MODE, // We skip the root mode because we don't want to emit the DOCTYPE in legacy mode.
110-
selectedValue: null,
111-
noscriptTagInScope: false,
112-
};
113-
}
105+
import {
106+
stringToChunk,
107+
stringToPrecomputedChunk,
108+
} from 'react-server/src/ReactServerStreamConfig';
109+
110+
// this chunk is empty on purpose because we do not want to emit the DOCTYPE in legacy mode
111+
export const doctypeChunk: PrecomputedChunk = stringToPrecomputedChunk('');
114112

115113
export type {
116114
Resources,
@@ -138,6 +136,7 @@ export {
138136
writeResourcesForBoundary,
139137
writePlaceholder,
140138
writeCompletedRoot,
139+
createRootFormatContext,
141140
createResources,
142141
createBoundaryResources,
143142
writePreamble,
@@ -148,8 +147,6 @@ export {
148147
prepareHostDispatcher,
149148
} from './ReactFizzConfigDOM';
150149

151-
import {stringToChunk} from 'react-server/src/ReactServerStreamConfig';
152-
153150
import escapeTextForBrowser from './escapeTextForBrowser';
154151

155152
export function pushTextInstance(
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @emails react-core
8+
* @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment
9+
*/
10+
11+
'use strict';
12+
13+
let React;
14+
let ReactDOMFizzServer;
15+
16+
describe('ReactDOMFloat', () => {
17+
beforeEach(() => {
18+
jest.resetModules();
19+
20+
React = require('react');
21+
ReactDOMFizzServer = require('react-dom/server');
22+
});
23+
24+
// fixes #27177
25+
// @gate enableFloat
26+
it('does not hoist above the <html> tag', async () => {
27+
const result = ReactDOMFizzServer.renderToString(
28+
<html>
29+
<head>
30+
<script src="foo" />
31+
<meta charSet="utf-8" />
32+
<title>title</title>
33+
</head>
34+
</html>,
35+
);
36+
37+
expect(result).toEqual(
38+
'<html><head><meta charSet="utf-8"/><title>title</title><script src="foo"></script></head></html>',
39+
);
40+
});
41+
});

0 commit comments

Comments
 (0)