Skip to content

Commit 8745eec

Browse files
committed
[compiler] Normalize whitespace in JSX string attributes for builtin 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
1 parent 4610359 commit 8745eec

9 files changed

Lines changed: 212 additions & 1 deletion

compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1712,9 +1712,10 @@ function codegenInstructionValue(
17121712
break;
17131713
}
17141714
case 'JsxExpression': {
1715+
const isBuiltinTag = instrValue.tag.kind === 'BuiltinTag';
17151716
const attributes: Array<t.JSXAttribute | t.JSXSpreadAttribute> = [];
17161717
for (const attribute of instrValue.props) {
1717-
attributes.push(codegenJsxAttribute(cx, attribute));
1718+
attributes.push(codegenJsxAttribute(cx, attribute, isBuiltinTag));
17181719
}
17191720
let tagValue =
17201721
instrValue.tag.kind === 'Identifier'
@@ -2123,9 +2124,21 @@ function codegenInstructionValue(
21232124
*/
21242125
const STRING_REQUIRES_EXPR_CONTAINER_PATTERN =
21252126
/[\u{0000}-\u{001F}\u{007F}\u{0080}-\u{FFFF}\u{010000}-\u{10FFFF}]|"|\\/u;
2127+
2128+
/**
2129+
* Browsers normalize tab, newline, and carriage return characters to spaces
2130+
* when parsing HTML attribute values. Without this normalization, the compiled
2131+
* code preserves these characters while the browser normalizes the server-
2132+
* rendered HTML, causing a hydration mismatch.
2133+
*
2134+
* See: https://html.spec.whatwg.org/multipage/parsing.html#attribute-value-(double-quoted)-state
2135+
*/
2136+
const ATTRIBUTE_WHITESPACE_NORMALIZE_PATTERN = /[\t\n\r]/g;
2137+
21262138
function codegenJsxAttribute(
21272139
cx: Context,
21282140
attribute: JsxAttribute,
2141+
isBuiltinTag: boolean,
21292142
): t.JSXAttribute | t.JSXSpreadAttribute {
21302143
switch (attribute.kind) {
21312144
case 'JsxAttribute': {
@@ -2145,6 +2158,18 @@ function codegenJsxAttribute(
21452158
switch (innerValue.type) {
21462159
case 'StringLiteral': {
21472160
value = innerValue;
2161+
if (
2162+
isBuiltinTag &&
2163+
!cx.fbtOperands.has(attribute.place.identifier.id)
2164+
) {
2165+
const normalized = value.value.replace(
2166+
ATTRIBUTE_WHITESPACE_NORMALIZE_PATTERN,
2167+
' ',
2168+
);
2169+
if (normalized !== value.value) {
2170+
value = createStringLiteral(value.loc, normalized);
2171+
}
2172+
}
21482173
if (
21492174
STRING_REQUIRES_EXPR_CONTAINER_PATTERN.test(value.value) &&
21502175
!cx.fbtOperands.has(attribute.place.identifier.id)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
2+
## Input
3+
4+
```javascript
5+
function Component() {
6+
return (
7+
<div
8+
className={"foo\nbar"}
9+
>
10+
Hello
11+
</div>
12+
);
13+
}
14+
15+
```
16+
17+
## Code
18+
19+
```javascript
20+
import { c as _c } from "react/compiler-runtime";
21+
function Component() {
22+
const $ = _c(1);
23+
let t0;
24+
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
25+
t0 = <div className="foo bar">Hello</div>;
26+
$[0] = t0;
27+
} else {
28+
t0 = $[0];
29+
}
30+
return t0;
31+
}
32+
33+
```
34+
35+
### Eval output
36+
(kind: exception) Fixture not implemented
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
function Component() {
2+
return (
3+
<div
4+
className={"foo\nbar"}
5+
>
6+
Hello
7+
</div>
8+
);
9+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
2+
## Input
3+
4+
```javascript
5+
function Component() {
6+
return (
7+
<div
8+
className="foo
9+
bar
10+
baz"
11+
>
12+
Hello
13+
</div>
14+
);
15+
}
16+
17+
```
18+
19+
## Code
20+
21+
```javascript
22+
import { c as _c } from "react/compiler-runtime";
23+
function Component() {
24+
const $ = _c(1);
25+
let t0;
26+
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
27+
t0 = <div className="foo bar baz">Hello</div>;
28+
$[0] = t0;
29+
} else {
30+
t0 = $[0];
31+
}
32+
return t0;
33+
}
34+
35+
```
36+
37+
### Eval output
38+
(kind: exception) Fixture not implemented
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
function Component() {
2+
return (
3+
<div
4+
className="foo
5+
bar
6+
baz"
7+
>
8+
Hello
9+
</div>
10+
);
11+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
2+
## Input
3+
4+
```javascript
5+
function Component() {
6+
return (
7+
<div
8+
className={"foo\tbar\r\nbaz"}
9+
>
10+
Hello
11+
</div>
12+
);
13+
}
14+
15+
```
16+
17+
## Code
18+
19+
```javascript
20+
import { c as _c } from "react/compiler-runtime";
21+
function Component() {
22+
const $ = _c(1);
23+
let t0;
24+
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
25+
t0 = <div className="foo bar baz">Hello</div>;
26+
$[0] = t0;
27+
} else {
28+
t0 = $[0];
29+
}
30+
return t0;
31+
}
32+
33+
```
34+
35+
### Eval output
36+
(kind: exception) Fixture not implemented
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
function Component() {
2+
return (
3+
<div
4+
className={"foo\tbar\r\nbaz"}
5+
>
6+
Hello
7+
</div>
8+
);
9+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
2+
## Input
3+
4+
```javascript
5+
function Component() {
6+
return (
7+
<div
8+
title="line1
9+
line2"
10+
>
11+
Hello
12+
</div>
13+
);
14+
}
15+
16+
```
17+
18+
## Code
19+
20+
```javascript
21+
import { c as _c } from "react/compiler-runtime";
22+
function Component() {
23+
const $ = _c(1);
24+
let t0;
25+
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
26+
t0 = <div title="line1 line2">Hello</div>;
27+
$[0] = t0;
28+
} else {
29+
t0 = $[0];
30+
}
31+
return t0;
32+
}
33+
34+
```
35+
36+
### Eval output
37+
(kind: exception) Fixture not implemented
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
function Component() {
2+
return (
3+
<div
4+
title="line1
5+
line2"
6+
>
7+
Hello
8+
</div>
9+
);
10+
}

0 commit comments

Comments
 (0)