@samitouri / QOS-React / commits / fc08d76031

[compiler] Fix JSX tags prefixed with `_` or `$` incorrectly treated as host elements (#36688)

## Summary Fixes #36601 ### Problem In `lowerJsxElementName` (BuildHIR.ts), the condition `if (tag.match(/^[A-Z]/))` only treats JSX tags starting with an **uppercase letter** as component references. Tags starting with `_`, `$`, or any other non-letter character fell through to the `else` branch and were incorrectly classified as `BuiltinTag` (host/intrinsic elements). This meant that `<_Bar />` or `<$Foo />` was treated like `<div />`, causing the compiler to skip memoization of the component and potentially producing incorrect output. ### Root Cause JSX semantics (as implemented by Babel's JSX transform) are: - Tag starts with **lowercase** letter → host/intrinsic element (string tag) - **Everything else** → component reference (in-scope identifier) The original code only handled the first half of that rule ("starts with uppercase → component") while ignoring identifiers like `_Bar` and `$Foo`. ### Fix Change: ```ts if (tag.match(/^[A-Z]/)) { ``` To: ```ts if (!tag.match(/^[a-z]/)) { ``` This correctly classifies any JSX identifier that does NOT start with a lowercase letter as a component reference, matching JSX spec semantics. ### Test Added fixture `jsx-underscore-prefix-component` that renders `<_Bar />` and verifies the compiler correctly memoizes it as a component reference. ## How did you test this change? - Added new compiler fixture test: `jsx-underscore-prefix-component` - Ran `yarn workspace babel-plugin-react-compiler lint` ✅ - Ran snap tests with the new fixture to generate expected output ✅

Dmitrii committed Jul 7, 2026 at 06:24 UTC fc08d760317d909dbda478e8f9b577dced1f5954
3 files changed +73 -1
compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts
+1 -1
@@ -3409,7 +3409,7 @@ function lowerJsxElementName(
3409 const exprLoc = exprNode.loc ?? GeneratedSource;
3410 if (exprPath.isJSXIdentifier()) {
3411 const tag: string = exprPath.node.name;
3412 - if (tag.match(/^[A-Z]/)) {
3412 + if (!tag.match(/^[a-z]/)) {
3413 const kind = getLoadKind(builder, exprPath);
3414 return lowerValueToTemporary(builder, {
3415 kind: kind,
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-underscore-prefix-component.expect.md new
+56
@@ -0,0 +1,56 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// Test that JSX tags starting with `_` or `$` are treated as component
6 +// references (not host/builtin elements). JSX semantics: any tag NOT starting
7 +// with a lowercase letter is a component reference.
8 +
9 +import {Stringify} from 'shared-runtime';
10 +
11 +const _Bar = Stringify;
12 +
13 +function Component(props) {
14 + return <_Bar value={props.value} />;
15 +}
16 +
17 +export const FIXTURE_ENTRYPOINT = {
18 + fn: Component,
19 + params: [{value: 42}],
20 +};
21 +
22 +```
23 +
24 +## Code
25 +
26 +```javascript
27 +import { c as _c } from "react/compiler-runtime"; // Test that JSX tags starting with `_` or `$` are treated as component
28 +// references (not host/builtin elements). JSX semantics: any tag NOT starting
29 +// with a lowercase letter is a component reference.
30 +
31 +import { Stringify } from "shared-runtime";
32 +
33 +const _Bar = Stringify;
34 +
35 +function Component(props) {
36 + const $ = _c(2);
37 + let t0;
38 + if ($[0] !== props.value) {
39 + t0 = <_Bar value={props.value} />;
40 + $[0] = props.value;
41 + $[1] = t0;
42 + } else {
43 + t0 = $[1];
44 + }
45 + return t0;
46 +}
47 +
48 +export const FIXTURE_ENTRYPOINT = {
49 + fn: Component,
50 + params: [{ value: 42 }],
51 +};
52 +
53 +```
54 +
55 +### Eval output
56 +(kind: ok) <div>{"value":42}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-underscore-prefix-component.js new
+16
@@ -0,0 +1,16 @@
1 +// Test that JSX tags starting with `_` or `$` are treated as component
2 +// references (not host/builtin elements). JSX semantics: any tag NOT starting
3 +// with a lowercase letter is a component reference.
4 +
5 +import {Stringify} from 'shared-runtime';
6 +
7 +const _Bar = Stringify;
8 +
9 +function Component(props) {
10 + return <_Bar value={props.value} />;
11 +}
12 +
13 +export const FIXTURE_ENTRYPOINT = {
14 + fn: Component,
15 + params: [{value: 42}],
16 +};