Skip to content

[compiler] Normalize whitespace in JSX string attributes for builtin tags#671

Open
everettbu wants to merge 3 commits into
mainfrom
fix/compiler-jsx-attr-whitespace
Open

[compiler] Normalize whitespace in JSX string attributes for builtin tags#671
everettbu wants to merge 3 commits into
mainfrom
fix/compiler-jsx-attr-whitespace

Conversation

@everettbu

@everettbu everettbu commented Mar 8, 2026

Copy link
Copy Markdown

Mirror of facebook/react#35978
Original author: Felipeness


Summary

Fixes #35481

Root cause

The Babel JSX transform (``@babel/plugin-transform-react-jsx) normalizes newlines followed by whitespace in plain JSX string attributes:

// `@babel`/plugin-transform-react-jsx/lib/create-plugin.js:260-263
if (isStringLiteral(value) && !isJSXExpressionContainer(attribute.node.value)) {
  value.value = value.value.replace(/\n\s+/g, " ");
  delete value.extra?.raw;
}

This normalization is skipped for JSXExpressionContainer values. When the React Compiler wraps strings in expression containers (because \n matches STRING_REQUIRES_EXPR_CONTAINER_PATTERN in the U+0000-U+001F range), the JSX transform bypasses normalization. The server code (uncompiled) goes through the standard normalization, creating a hydration mismatch.

Fix

Apply the same /\n\s+/g" " normalization that the JSX transform uses, before the expression container check. This ensures compiled output matches the behavior of non-compiled code. After normalization, strings that no longer contain control characters avoid unnecessary expression container wrapping entirely.

Scope

  • Builtin HTML elements only (div, span, etc.) — component props are preserved unchanged
  • fbt operands excluded — internationalization strings are not modified
  • Uses the exact same regex as ``@babel/plugin-transform-react-jsx

Before (compiled output)

// Input: className="\n        flex min-h-screen\n        dark:bg-black\n      "
// Compiled: className={"\n        flex min-h-screen\n        dark:bg-black\n      "}
//           ↑ expression container bypasses JSX transform normalization → hydration mismatch

After (compiled output)

// Input: className="\n        flex min-h-screen\n        dark:bg-black\n      "
// Compiled: className=" flex min-h-screen dark:bg-black "
//           ↑ matches JSX transform output, no expression container needed

How did you test this change?

5 fixture tests covering positive and negative scenarios:

Fixture Scenario Expected behavior
repro-multiline-classname-hydration Reporter's exact case: multiline Tailwind className \n + indentation → single space
repro-multiline-classname-newline-indent Expression container {"foo\n bar\n baz"} \n\s+ normalized → "foo bar baz"
repro-multiline-title-attribute title with \n + indentation Same normalization as className
repro-multiline-classname-expression {"foo\nbar"}\n without trailing whitespace NOT normalized (no \s+ after \n, matches JSX transform)
repro-multiline-component-prop-preserved <MyComponent data={"\n"}> NOT normalized (component, not builtin tag)
yarn snap -p repro-multiline-classname-hydration       # 1 Passed
yarn snap -p repro-multiline-classname-newline-indent   # 1 Passed
yarn snap -p repro-multiline-title-attribute            # 1 Passed
yarn snap -p repro-multiline-classname-expression       # 1 Passed
yarn snap -p repro-multiline-component-prop-preserved   # 1 Passed
yarn workspace babel-plugin-react-compiler lint         # Clean

…tags

Browsers normalize tab, newline, and carriage return characters to spaces
when parsing HTML attribute values. Without this normalization, the compiled
code preserves these characters while the browser normalizes the server-
rendered HTML, causing a hydration mismatch.

This change normalizes \t, \n, and \r to spaces in JSX string attribute
values for builtin HTML elements (div, span, etc.) during codegen, while
preserving the original values for component props and fbt operands.

Fixes #35481
- Fix \r\n producing double space: normalize CRLF to LF first, then
  replace remaining \t\n\r with spaces
- Fix JSDoc: the real cause is Babel's code generator silently replacing
  newlines during serialization, not browser attribute normalization
- Add negative test: component props (non-builtin tags) must preserve
  \n unchanged
@greptile-apps

greptile-apps Bot commented Mar 8, 2026

Copy link
Copy Markdown

Greptile Summary

Fixes a hydration mismatch caused by the React Compiler wrapping string attributes in JSXExpressionContainer nodes, which bypasses the Babel JSX transform's \n\s+ → space normalization. The fix applies the same /\n\s+/g regex normalization that @babel/plugin-transform-react-jsx uses, scoped to builtin HTML elements only (excluding fbt operands and custom components).

  • Adds whitespace normalization in codegenJsxAttribute before the expression container check, ensuring compiled output matches non-compiled behavior
  • Correctly scoped: only applies to builtin tags (div, span, etc.), not custom components; fbt internationalization strings are preserved
  • After normalization, strings that no longer contain control characters avoid unnecessary expression container wrapping
  • 5 fixture tests cover positive cases (multiline className, title attribute, expression container with \n\s+) and negative cases (bare \n without trailing whitespace, component props)

Confidence Score: 5/5

  • This PR is safe to merge — it's a targeted, well-scoped fix that exactly mirrors Babel's existing normalization behavior.
  • The change is minimal and precisely targeted: a single normalization step added before the existing expression container check, using the exact same regex as Babel's JSX transform. The fix is correctly scoped to builtin HTML elements only, preserves fbt operands, and has thorough test coverage with both positive and negative cases. There are no architectural concerns or risk of regressions.
  • No files require special attention.

Important Files Changed

Filename Overview
compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts Adds JSX string attribute whitespace normalization for builtin tags, matching Babel's /\n\s+/g regex. Correctly scoped to builtin HTML elements and excludes fbt operands. New isBuiltinTag parameter threaded to codegenJsxAttribute.
compiler/packages/babel-plugin-react-compiler/src/tests/fixtures/compiler/repro-multiline-classname-hydration.expect.md Test fixture for the primary hydration mismatch case: multiline Tailwind className is correctly normalized to a single-line string.
compiler/packages/babel-plugin-react-compiler/src/tests/fixtures/compiler/repro-multiline-classname-expression.expect.md Negative test: \n without trailing whitespace is NOT normalized (matches Babel's regex behavior).
compiler/packages/babel-plugin-react-compiler/src/tests/fixtures/compiler/repro-multiline-component-prop-preserved.expect.md Negative test: component props (non-builtin tags) are not normalized.

Last reviewed commit: 4574136

The actual root cause is @babel/plugin-transform-react-jsx normalizing
/\n\s+/g → " " for plain JSX string attributes but skipping this for
JSXExpressionContainer values. The compiler wraps strings in expression
containers, bypassing the normalization and creating a mismatch.

Use the same regex /\n\s+/g as the JSX transform to produce identical
output. This replaces the incorrect /[\t\n\r]/g which over-normalized
\t and \r (untouched by JSX transform) and diverged in output for all
whitespace patterns.

Rewrite fixtures to test the reporter's actual scenario (newline +
indentation in Tailwind className).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants