From df830692eac2737a930cb952b15d70c994a00154 Mon Sep 17 00:00:00 2001 From: Josh Story Date: Tue, 22 Aug 2023 10:54:33 -0700 Subject: [PATCH] [Float][Fizz][Legacy] hoisted elements no longer emit before `` 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 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 tag which is trivial enough Fixes #27177 --- .../src/server/ReactFizzConfigDOM.js | 9 ++-- .../src/server/ReactFizzConfigDOMLegacy.js | 19 ++++----- .../src/__tests__/ReactDOMLegacyFloat-test.js | 41 +++++++++++++++++++ 3 files changed, 55 insertions(+), 14 deletions(-) create mode 100644 packages/react-dom/src/__tests__/ReactDOMLegacyFloat-test.js diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js index 952b9a93bda3f..a30a9af90a66d 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOM.js @@ -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 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; @@ -3027,7 +3027,10 @@ function startChunkForTag(tag: string): PrecomputedChunk { return tagStartChunk; } -const DOCTYPE: PrecomputedChunk = stringToPrecomputedChunk(''); +export const doctypeChunk: PrecomputedChunk = + stringToPrecomputedChunk(''); + +import {doctypeChunk as DOCTYPE} from 'react-server/src/ReactFizzConfig'; export function pushStartInstance( target: Array, diff --git a/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js b/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js index d8f7094b50068..4907a0f8209bf 100644 --- a/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js +++ b/packages/react-dom-bindings/src/server/ReactFizzConfigDOMLegacy.js @@ -11,7 +11,6 @@ import type { Resources, BootstrapScriptDescriptor, ExternalRuntimeScript, - FormatContext, StreamingFormat, InstructionState, } from './ReactFizzConfigDOM'; @@ -24,7 +23,6 @@ import { writeStartClientRenderedSuspenseBoundary as writeStartClientRenderedSuspenseBoundaryImpl, writeEndCompletedSuspenseBoundary as writeEndCompletedSuspenseBoundaryImpl, writeEndClientRenderedSuspenseBoundary as writeEndClientRenderedSuspenseBoundaryImpl, - HTML_MODE, } from './ReactFizzConfigDOM'; import type { @@ -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, @@ -138,6 +136,7 @@ export { writeResourcesForBoundary, writePlaceholder, writeCompletedRoot, + createRootFormatContext, createResources, createBoundaryResources, writePreamble, @@ -148,8 +147,6 @@ export { prepareHostDispatcher, } from './ReactFizzConfigDOM'; -import {stringToChunk} from 'react-server/src/ReactServerStreamConfig'; - import escapeTextForBrowser from './escapeTextForBrowser'; export function pushTextInstance( diff --git a/packages/react-dom/src/__tests__/ReactDOMLegacyFloat-test.js b/packages/react-dom/src/__tests__/ReactDOMLegacyFloat-test.js new file mode 100644 index 0000000000000..94876e41641f9 --- /dev/null +++ b/packages/react-dom/src/__tests__/ReactDOMLegacyFloat-test.js @@ -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 tag', async () => { + const result = ReactDOMFizzServer.renderToString( + + + ', + ); + }); +});