@samitouri / QOS-React-1 / commits / 6891dcb87d

[compiler] treat ref-like identifiers as refs by default (#34005)

`@enableTreatRefLikeIdentifiersAsRefs` is now on by default. I made one small fix to the render helper logic as part of this, uncovered by including more tests. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34005). * #34027 * #34026 * #34025 * #34024 * __->__ #34005

Joseph Savona committed Jul 29, 2025 at 10:51 UTC 6891dcb87db831db17ab109730dea63706875dd7
22 files changed +200 -161
compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts
+1 -1
@@ -608,7 +608,7 @@ export const EnvironmentConfigSchema = z.object({
608 *
609 * Here the variables `ref` and `myRef` will be typed as Refs.
610 */
611 - enableTreatRefLikeIdentifiersAsRefs: z.boolean().default(false),
611 + enableTreatRefLikeIdentifiersAsRefs: z.boolean().default(true),
612
613 /*
614 * If specified a value, the compiler lowers any calls to `useContext` to use
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts
+10 -23
@@ -432,15 +432,11 @@ function validateNoRefAccessInRenderImpl(
432 * By default we check that function call operands are not refs,
433 * ref values, or functions that can access refs.
434 */
435 - if (
436 - isRefLValue ||
437 - interpolatedAsJsx.has(instr.lvalue.identifier.id) ||
438 - hookKind != null
439 - ) {
435 + if (isRefLValue || hookKind != null) {
436 /**
437 * Special cases:
438 *
443 - * 1) the lvalue is a ref
439 + * 1. the lvalue is a ref
440 * In general passing a ref to a function may access that ref
441 * value during render, so we disallow it.
442 *
@@ -452,31 +448,22 @@ function validateNoRefAccessInRenderImpl(
448 *
449 * Eg `const mergedRef = mergeRefs(ref1, ref2)`
450 *
455 - * 2) the lvalue is passed as a jsx child
456 - *
457 - * For example `<Foo>{renderHelper(ref)}</Foo>`. Here we have more
458 - * context and infer that the ref is being passed to a component-like
459 - * render function which attempts to obey the rules.
460 - *
461 - * 3) hooks
451 + * 2. calling hooks
452 *
453 * Hooks are independently checked to ensure they don't access refs
454 * during render.
455 */
456 validateNoDirectRefValueAccess(errors, operand, env);
467 - } else if (!isRefLValue) {
457 + } else if (interpolatedAsJsx.has(instr.lvalue.identifier.id)) {
458 /**
469 - * In general passing a ref to a function may access that ref
470 - * value during render, so we disallow it.
459 + * Special case: the lvalue is passed as a jsx child
460 *
472 - * The main exception is the "mergeRefs" pattern, ie a function
473 - * that accepts multiple refs as arguments (or an array of refs)
474 - * and returns a new, aggregated ref. If the lvalue is a ref,
475 - * we assume that the user is doing this pattern and allow passing
476 - * refs.
477 - *
478 - * Eg `const mergedRef = mergeRefs(ref1, ref2)`
461 + * For example `<Foo>{renderHelper(ref)}</Foo>`. Here we have more
462 + * context and infer that the ref is being passed to a component-like
463 + * render function which attempts to obey the rules.
464 */
465 + validateNoRefValueAccess(errors, env, operand);
466 + } else {
467 validateNoRefPassedToFunction(
468 errors,
469 env,
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect-indirect.expect.md
+5 -2
@@ -27,6 +27,7 @@ function Component() {
27 }
28
29 function Child({ref}) {
30 + 'use no memo';
31 // This violates the rules of React, so we access the ref in a child
32 // component
33 return ref.current;
@@ -100,8 +101,10 @@ function Component() {
101 return t6;
102 }
103
103 -function Child(t0) {
104 - const { ref } = t0;
104 +function Child({ ref }) {
105 + "use no memo";
106 + // This violates the rules of React, so we access the ref in a child
107 + // component
108 return ref.current;
109 }
110
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect-indirect.js
+1
@@ -23,6 +23,7 @@ function Component() {
23 }
24
25 function Child({ref}) {
26 + 'use no memo';
27 // This violates the rules of React, so we access the ref in a child
28 // component
29 return ref.current;
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect.expect.md
+5 -2
@@ -23,6 +23,7 @@ function Component() {
23 }
24
25 function Child({ref}) {
26 + 'use no memo';
27 // This violates the rules of React, so we access the ref in a child
28 // component
29 return ref.current;
@@ -86,8 +87,10 @@ function Component() {
87 return t5;
88 }
89
89 -function Child(t0) {
90 - const { ref } = t0;
90 +function Child({ ref }) {
91 + "use no memo";
92 + // This violates the rules of React, so we access the ref in a child
93 + // component
94 return ref.current;
95 }
96
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect.js
+1
@@ -19,6 +19,7 @@ function Component() {
19 }
20
21 function Child({ref}) {
22 + 'use no memo';
23 // This violates the rules of React, so we access the ref in a child
24 // component
25 return ref.current;
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-unused-callback-nested.expect.md
+5 -2
@@ -25,6 +25,7 @@ function Component() {
25 }
26
27 function Child({ref}) {
28 + 'use no memo';
29 // This violates the rules of React, so we access the ref in a child
30 // component
31 return ref.current;
@@ -83,8 +84,10 @@ function Component() {
84 }
85 function _temp() {}
86
86 -function Child(t0) {
87 - const { ref } = t0;
87 +function Child({ ref }) {
88 + "use no memo";
89 + // This violates the rules of React, so we access the ref in a child
90 + // component
91 return ref.current;
92 }
93
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-unused-callback-nested.js
+1
@@ -21,6 +21,7 @@ function Component() {
21 }
22
23 function Child({ref}) {
24 + 'use no memo';
25 // This violates the rules of React, so we access the ref in a child
26 // component
27 return ref.current;
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-aliased-ref-in-callback-invoked-during-render-.expect.md
+1 -1
@@ -29,7 +29,7 @@ error.invalid-aliased-ref-in-callback-invoked-during-render-.ts:9:33
29 7 | return <Foo item={item} current={current} />;
30 8 | };
31 > 9 | return <Items>{props.items.map(item => renderItem(item))}</Items>;
32 - | ^^^^^^^^^^^^^^^^^^^^^^^^ Passing a ref to a function may read its value during render
32 + | ^^^^^^^^^^^^^^^^^^^^^^^^ Cannot access ref value during render
33 10 | }
34 11 |
35 ```
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-in-callback-invoked-during-render.expect.md
+1 -1
@@ -28,7 +28,7 @@ error.invalid-ref-in-callback-invoked-during-render.ts:8:33
28 6 | return <Foo item={item} current={current} />;
29 7 | };
30 > 8 | return <Items>{props.items.map(item => renderItem(item))}</Items>;
31 - | ^^^^^^^^^^^^^^^^^^^^^^^^ Passing a ref to a function may read its value during render
31 + | ^^^^^^^^^^^^^^^^^^^^^^^^ Cannot access ref value during render
32 9 | }
33 10 |
34 ```
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.repro-ref-mutable-range.expect.md deleted
-47
@@ -1,47 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -import {Stringify, identity, mutate, CONST_TRUE} from 'shared-runtime';
6 -
7 -function Foo(props, ref) {
8 - const value = {};
9 - if (CONST_TRUE) {
10 - mutate(value);
11 - return <Stringify ref={ref} />;
12 - }
13 - mutate(value);
14 - if (CONST_TRUE) {
15 - return <Stringify ref={identity(ref)} />;
16 - }
17 - return value;
18 -}
19 -
20 -export const FIXTURE_ENTRYPOINT = {
21 - fn: Foo,
22 - params: [{}, {current: 'fake-ref-object'}],
23 -};
24 -
25 -```
26 -
27 -
28 -## Error
29 -
30 -```
31 -Found 1 error:
32 -
33 -Error: Cannot access refs during render
34 -
35 -React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef)
36 -
37 -error.repro-ref-mutable-range.ts:11:36
38 - 9 | mutate(value);
39 - 10 | if (CONST_TRUE) {
40 -> 11 | return <Stringify ref={identity(ref)} />;
41 - | ^^^ Passing a ref to a function may read its value during render
42 - 12 | }
43 - 13 | return value;
44 - 14 | }
45 -```
46 -
47 -
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/bailout-retry/mutate-after-useeffect-ref-access.expect.md
+1 -1
@@ -47,7 +47,7 @@ export const FIXTURE_ENTRYPOINT = {
47 ## Logs
48
49 ```
50 -{"kind":"CompileError","fnLoc":{"start":{"line":6,"column":0,"index":158},"end":{"line":11,"column":1,"index":331},"filename":"mutate-after-useeffect-ref-access.ts"},"detail":{"options":{"severity":"InvalidReact","category":"This value cannot be modified","description":"Modifying component props or hook arguments is not allowed. Consider using a local variable instead.","details":[{"kind":"error","loc":{"start":{"line":9,"column":2,"index":289},"end":{"line":9,"column":16,"index":303},"filename":"mutate-after-useeffect-ref-access.ts"},"message":"value cannot be modified"}]}}}
50 +{"kind":"CompileError","fnLoc":{"start":{"line":6,"column":0,"index":158},"end":{"line":11,"column":1,"index":331},"filename":"mutate-after-useeffect-ref-access.ts"},"detail":{"options":{"severity":"InvalidReact","category":"Cannot access refs during render","description":"React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef)","details":[{"kind":"error","loc":{"start":{"line":9,"column":2,"index":289},"end":{"line":9,"column":16,"index":303},"filename":"mutate-after-useeffect-ref-access.ts"},"message":"Cannot update ref during render"}]}}}
51 {"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":8,"column":2,"index":237},"end":{"line":8,"column":50,"index":285},"filename":"mutate-after-useeffect-ref-access.ts"},"decorations":[{"start":{"line":8,"column":24,"index":259},"end":{"line":8,"column":30,"index":265},"filename":"mutate-after-useeffect-ref-access.ts","identifierName":"arrRef"}]}
52 {"kind":"CompileSuccess","fnLoc":{"start":{"line":6,"column":0,"index":158},"end":{"line":11,"column":1,"index":331},"filename":"mutate-after-useeffect-ref-access.ts"},"fnName":"Component","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0}
53 ```
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-effect-dependencies/nonreactive-ref-helper.expect.md
+2 -2
@@ -51,12 +51,12 @@ function RefsInEffects() {
51 const ref = useRefHelper();
52 const wrapped = useDeeperRefHelper();
53 let t0;
54 - if ($[0] !== ref.current || $[1] !== wrapped.foo.current) {
54 + if ($[0] !== ref || $[1] !== wrapped.foo.current) {
55 t0 = () => {
56 print(ref.current);
57 print(wrapped.foo.current);
58 };
59 - $[0] = ref.current;
59 + $[0] = ref;
60 $[1] = wrapped.foo.current;
61 $[2] = t0;
62 } else {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/new-mutability/mutate-after-useeffect-ref-access.expect.md
+1 -1
@@ -47,7 +47,7 @@ export const FIXTURE_ENTRYPOINT = {
47 ## Logs
48
49 ```
50 -{"kind":"CompileError","fnLoc":{"start":{"line":6,"column":0,"index":190},"end":{"line":11,"column":1,"index":363},"filename":"mutate-after-useeffect-ref-access.ts"},"detail":{"options":{"severity":"InvalidReact","category":"This value cannot be modified","description":"Modifying component props or hook arguments is not allowed. Consider using a local variable instead.","details":[{"kind":"error","loc":{"start":{"line":9,"column":2,"index":321},"end":{"line":9,"column":16,"index":335},"filename":"mutate-after-useeffect-ref-access.ts"},"message":"value cannot be modified"}]}}}
50 +{"kind":"CompileError","fnLoc":{"start":{"line":6,"column":0,"index":190},"end":{"line":11,"column":1,"index":363},"filename":"mutate-after-useeffect-ref-access.ts"},"detail":{"options":{"severity":"InvalidReact","category":"Cannot access refs during render","description":"React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef)","details":[{"kind":"error","loc":{"start":{"line":9,"column":2,"index":321},"end":{"line":9,"column":16,"index":335},"filename":"mutate-after-useeffect-ref-access.ts"},"message":"Cannot update ref during render"}]}}}
51 {"kind":"AutoDepsDecorations","fnLoc":{"start":{"line":8,"column":2,"index":269},"end":{"line":8,"column":50,"index":317},"filename":"mutate-after-useeffect-ref-access.ts"},"decorations":[{"start":{"line":8,"column":24,"index":291},"end":{"line":8,"column":30,"index":297},"filename":"mutate-after-useeffect-ref-access.ts","identifierName":"arrRef"}]}
52 {"kind":"CompileSuccess","fnLoc":{"start":{"line":6,"column":0,"index":190},"end":{"line":11,"column":1,"index":363},"filename":"mutate-after-useeffect-ref-access.ts"},"fnName":"Component","memoSlots":0,"memoBlocks":0,"memoValues":0,"prunedMemoBlocks":0,"prunedMemoValues":0}
53 ```
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-invalid-useCallback-read-maybeRef.expect.md deleted
-39
@@ -1,39 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -// @validatePreserveExistingMemoizationGuarantees
6 -import {useCallback} from 'react';
7 -
8 -function useHook(maybeRef) {
9 - return useCallback(() => {
10 - return [maybeRef.current];
11 - }, [maybeRef]);
12 -}
13 -
14 -```
15 -
16 -
17 -## Error
18 -
19 -```
20 -Found 1 error:
21 -
22 -Memoization: Compilation skipped because existing memoization could not be preserved
23 -
24 -React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `maybeRef.current`, but the source dependencies were [maybeRef]. Differences in ref.current access.
25 -
26 -error.maybe-invalid-useCallback-read-maybeRef.ts:5:21
27 - 3 |
28 - 4 | function useHook(maybeRef) {
29 -> 5 | return useCallback(() => {
30 - | ^^^^^^^
31 -> 6 | return [maybeRef.current];
32 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
33 -> 7 | }, [maybeRef]);
34 - | ^^^^ Could not preserve existing manual memoization
35 - 8 | }
36 - 9 |
37 -```
38 -
39 -
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.maybe-invalid-useMemo-read-maybeRef.expect.md deleted
-39
@@ -1,39 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -// @validatePreserveExistingMemoizationGuarantees
6 -import {useMemo} from 'react';
7 -
8 -function useHook(maybeRef, shouldRead) {
9 - return useMemo(() => {
10 - return () => [maybeRef.current];
11 - }, [shouldRead, maybeRef]);
12 -}
13 -
14 -```
15 -
16 -
17 -## Error
18 -
19 -```
20 -Found 1 error:
21 -
22 -Memoization: Compilation skipped because existing memoization could not be preserved
23 -
24 -React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `maybeRef.current`, but the source dependencies were [shouldRead, maybeRef]. Differences in ref.current access.
25 -
26 -error.maybe-invalid-useMemo-read-maybeRef.ts:5:17
27 - 3 |
28 - 4 | function useHook(maybeRef, shouldRead) {
29 -> 5 | return useMemo(() => {
30 - | ^^^^^^^
31 -> 6 | return () => [maybeRef.current];
32 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
33 -> 7 | }, [shouldRead, maybeRef]);
34 - | ^^^^ Could not preserve existing manual memoization
35 - 8 | }
36 - 9 |
37 -```
38 -
39 -
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/repro-maybe-invalid-useCallback-read-maybeRef.expect.md new
+38
@@ -0,0 +1,38 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +import {useCallback} from 'react';
7 +
8 +function useHook(maybeRef) {
9 + return useCallback(() => {
10 + return [maybeRef.current];
11 + }, [maybeRef]);
12 +}
13 +
14 +```
15 +
16 +## Code
17 +
18 +```javascript
19 +import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
20 +import { useCallback } from "react";
21 +
22 +function useHook(maybeRef) {
23 + const $ = _c(2);
24 + let t0;
25 + if ($[0] !== maybeRef) {
26 + t0 = () => [maybeRef.current];
27 + $[0] = maybeRef;
28 + $[1] = t0;
29 + } else {
30 + t0 = $[1];
31 + }
32 + return t0;
33 +}
34 +
35 +```
36 +
37 +### Eval output
38 +(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/repro-maybe-invalid-useCallback-read-maybeRef.ts renamed
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/repro-maybe-invalid-useMemo-read-maybeRef.expect.md new
+38
@@ -0,0 +1,38 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validatePreserveExistingMemoizationGuarantees
6 +import {useMemo} from 'react';
7 +
8 +function useHook(maybeRef, shouldRead) {
9 + return useMemo(() => {
10 + return () => [maybeRef.current];
11 + }, [shouldRead, maybeRef]);
12 +}
13 +
14 +```
15 +
16 +## Code
17 +
18 +```javascript
19 +import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees
20 +import { useMemo } from "react";
21 +
22 +function useHook(maybeRef, shouldRead) {
23 + const $ = _c(2);
24 + let t0;
25 + if ($[0] !== maybeRef) {
26 + t0 = () => [maybeRef.current];
27 + $[0] = maybeRef;
28 + $[1] = t0;
29 + } else {
30 + t0 = $[1];
31 + }
32 + return t0;
33 +}
34 +
35 +```
36 +
37 +### Eval output
38 +(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/repro-maybe-invalid-useMemo-read-maybeRef.ts renamed
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-ref-mutable-range.expect.md new
+89
@@ -0,0 +1,89 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +import {Stringify, identity, mutate, CONST_TRUE} from 'shared-runtime';
6 +
7 +function Foo(props, ref) {
8 + const value = {};
9 + if (CONST_TRUE) {
10 + mutate(value);
11 + return <Stringify ref={ref} />;
12 + }
13 + mutate(value);
14 + if (CONST_TRUE) {
15 + return <Stringify ref={identity(ref)} />;
16 + }
17 + return value;
18 +}
19 +
20 +export const FIXTURE_ENTRYPOINT = {
21 + fn: Foo,
22 + params: [{}, {current: 'fake-ref-object'}],
23 +};
24 +
25 +```
26 +
27 +## Code
28 +
29 +```javascript
30 +import { c as _c } from "react/compiler-runtime";
31 +import { Stringify, identity, mutate, CONST_TRUE } from "shared-runtime";
32 +
33 +function Foo(props, ref) {
34 + const $ = _c(7);
35 + let t0;
36 + let value;
37 + if ($[0] !== ref) {
38 + t0 = Symbol.for("react.early_return_sentinel");
39 + bb0: {
40 + value = {};
41 + if (CONST_TRUE) {
42 + mutate(value);
43 + t0 = <Stringify ref={ref} />;
44 + break bb0;
45 + }
46 +
47 + mutate(value);
48 + }
49 + $[0] = ref;
50 + $[1] = t0;
51 + $[2] = value;
52 + } else {
53 + t0 = $[1];
54 + value = $[2];
55 + }
56 + if (t0 !== Symbol.for("react.early_return_sentinel")) {
57 + return t0;
58 + }
59 + if (CONST_TRUE) {
60 + let t1;
61 + if ($[3] !== ref) {
62 + t1 = identity(ref);
63 + $[3] = ref;
64 + $[4] = t1;
65 + } else {
66 + t1 = $[4];
67 + }
68 + let t2;
69 + if ($[5] !== t1) {
70 + t2 = <Stringify ref={t1} />;
71 + $[5] = t1;
72 + $[6] = t2;
73 + } else {
74 + t2 = $[6];
75 + }
76 + return t2;
77 + }
78 + return value;
79 +}
80 +
81 +export const FIXTURE_ENTRYPOINT = {
82 + fn: Foo,
83 + params: [{}, { current: "fake-ref-object" }],
84 +};
85 +
86 +```
87 +
88 +### Eval output
89 +(kind: ok) <div>{"ref":{"current":"fake-ref-object"}}</div>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-ref-mutable-range.tsx renamed