Skip to content

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

Open
Felipeness wants to merge 3 commits intofacebook:mainfrom
Felipeness:fix/compiler-jsx-attr-whitespace
Open

[compiler] Normalize whitespace in JSX string attributes for builtin tags#35978
Felipeness wants to merge 3 commits intofacebook:mainfrom
Felipeness:fix/compiler-jsx-attr-whitespace

Conversation

@Felipeness
Copy link

@Felipeness Felipeness commented Mar 8, 2026

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 facebook#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
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.

[Compiler Bug]: Hydration error with multiline className

1 participant