@samitouri / QOS-React-1 / commits / 85bbe39ef8

[compiler] Fixes to enableTreatRefLikeIdentifiersAsRefs (#34000)

We added the `@enableTreatRefLikeIdentifiersAsRefs` feature a while back but never enabled it. Since then we've continued to see examples that motivate this mode, so here we're fixing it up to prepare to enable by default. It now works as follows: * If we find a property load or property store where both a) the object's name is ref-like (`ref` or `-Ref`) and b) the property is `current`, we infer the object itself as a ref and the value of the property as a ref value. Originally the feature only detected property loads, not stores. * Inferred refs are not considered stable (this is a change from the original implementation). The only way to get a stable ref is by calling `useRef()`. We've seen issues with assuming refs are stable. With this change, cases like the following now correctly error: ```js function Foo(props) { const fooRef = props.fooRef; fooRef.current = true; ^^^^^^^^^^^^^^ cannot modify ref in render } ``` --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34000). * #34027 * #34026 * #34025 * #34024 * #34005 * #34006 * #34004 * #34003 * __->__ #34000

Joseph Savona committed Jul 29, 2025 at 09:57 UTC 85bbe39ef8e24a192b5e9f2987b1babf8ce772e1
12 files changed +114 -40
compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts
+2
@@ -1211,6 +1211,8 @@ addObject(BUILTIN_SHAPES, BuiltInRefValueId, [
1211 ['*', {kind: 'Object', shapeId: BuiltInRefValueId}],
1212 ]);
1213
1214 +addObject(BUILTIN_SHAPES, ReanimatedSharedValueId, []);
1215 +
1216 addFunction(
1217 BUILTIN_SHAPES,
1218 [],
compiler/packages/babel-plugin-react-compiler/src/Inference/InferReactivePlaces.ts
-8
@@ -21,7 +21,6 @@ import {
21 isStableType,
22 isStableTypeContainer,
23 isUseOperator,
24 - isUseRefType,
24 } from '../HIR';
25 import {PostDominator} from '../HIR/Dominator';
26 import {
@@ -70,13 +69,6 @@ class StableSidemap {
69 isStable: false,
70 });
71 }
73 - } else if (
74 - this.env.config.enableTreatRefLikeIdentifiersAsRefs &&
75 - isUseRefType(lvalue.identifier)
76 - ) {
77 - this.map.set(lvalue.identifier.id, {
78 - isStable: true,
79 - });
72 }
73 break;
74 }
compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts
+30 -1
@@ -466,7 +466,36 @@ function* generateInstructionTypes(
466 yield equation(left, returnType);
467 break;
468 }
469 - case 'PropertyStore':
469 + case 'PropertyStore': {
470 + /**
471 + * Infer types based on assignments to known object properties
472 + * This is important for refs, where assignment to `<maybeRef>.current`
473 + * can help us infer that an object itself is a ref
474 + */
475 + yield equation(
476 + /**
477 + * Our property type declarations are best-effort and we haven't tested
478 + * using them to drive inference of rvalues from lvalues. We want to emit
479 + * a Property type in order to infer refs from `.current` accesses, but
480 + * stay conservative by not otherwise inferring anything about rvalues.
481 + * So we use a dummy type here.
482 + *
483 + * TODO: consider using the rvalue type here
484 + */
485 + makeType(),
486 + // unify() only handles properties in the second position
487 + {
488 + kind: 'Property',
489 + objectType: value.object.identifier.type,
490 + objectName: getName(names, value.object.identifier.id),
491 + propertyName: {
492 + kind: 'literal',
493 + value: value.property,
494 + },
495 + },
496 + );
497 + break;
498 + }
499 case 'DeclareLocal':
500 case 'RegExpLiteral':
501 case 'MetaProperty':
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-assign-current-inferred-ref-during-render.expect.md new
+34
@@ -0,0 +1,34 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @flow @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
6 +import {makeObject_Primitives} from 'shared-runtime';
7 +
8 +component Example() {
9 + const fooRef = makeObject_Primitives();
10 + fooRef.current = true;
11 +
12 + return <Stringify foo={fooRef} />;
13 +}
14 +
15 +```
16 +
17 +
18 +## Error
19 +
20 +```
21 +Found 1 error:
22 +
23 +Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
24 +
25 + 4 | component Example() {
26 + 5 | const fooRef = makeObject_Primitives();
27 +> 6 | fooRef.current = true;
28 + | ^^^^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)
29 + 7 |
30 + 8 | return <Stringify foo={fooRef} />;
31 + 9 | }
32 +```
33 +
34 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-assign-current-inferred-ref-during-render.js new
+9
@@ -0,0 +1,9 @@
1 +// @flow @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
2 +import {makeObject_Primitives} from 'shared-runtime';
3 +
4 +component Example() {
5 + const fooRef = makeObject_Primitives();
6 + fooRef.current = true;
7 +
8 + return <Stringify foo={fooRef} />;
9 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reanimated-no-memo-arg.expect.md
+2 -2
@@ -3,7 +3,7 @@
3
4 ```javascript
5 // @enableCustomTypeDefinitionForReanimated
6 -import {useAnimatedProps} from 'react-native-reanimated';
6 +import {useAnimatedProps, useSharedValue} from 'react-native-reanimated';
7 function Component() {
8 const radius = useSharedValue(50);
9
@@ -39,7 +39,7 @@ export const FIXTURE_ENTRYPOINT = {
39
40 ```javascript
41 import { c as _c } from "react/compiler-runtime"; // @enableCustomTypeDefinitionForReanimated
42 -import { useAnimatedProps } from "react-native-reanimated";
42 +import { useAnimatedProps, useSharedValue } from "react-native-reanimated";
43 function Component() {
44 const $ = _c(2);
45 const radius = useSharedValue(50);
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reanimated-no-memo-arg.js
+1 -1
@@ -1,5 +1,5 @@
1 // @enableCustomTypeDefinitionForReanimated
2 -import {useAnimatedProps} from 'react-native-reanimated';
2 +import {useAnimatedProps, useSharedValue} from 'react-native-reanimated';
3 function Component() {
4 const radius = useSharedValue(50);
5
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-like-name-in-effect.expect.md
+14 -10
@@ -47,28 +47,32 @@ function useCustomRef() {
47 function _temp() {}
48
49 function Foo() {
50 - const $ = _c(3);
50 + const $ = _c(4);
51 const ref = useCustomRef();
52 let t0;
53 - let t1;
54 - if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
53 + if ($[0] !== ref) {
54 t0 = () => {
55 ref.current?.click();
56 };
57 + $[0] = ref;
58 + $[1] = t0;
59 + } else {
60 + t0 = $[1];
61 + }
62 + let t1;
63 + if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
64 t1 = [];
59 - $[0] = t0;
60 - $[1] = t1;
65 + $[2] = t1;
66 } else {
62 - t0 = $[0];
63 - t1 = $[1];
67 + t1 = $[2];
68 }
69 useEffect(t0, t1);
70 let t2;
67 - if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
71 + if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
72 t2 = <div>foo</div>;
69 - $[2] = t2;
73 + $[3] = t2;
74 } else {
71 - t2 = $[2];
75 + t2 = $[3];
76 }
77 return t2;
78 }
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-like-name-in-useCallback-2.expect.md
+10 -8
@@ -14,7 +14,7 @@ function Foo() {
14
15 const onClick = useCallback(() => {
16 ref.current?.click();
17 - }, []);
17 + }, [ref]);
18
19 return <button onClick={onClick} />;
20 }
@@ -47,24 +47,26 @@ function useCustomRef() {
47 function _temp() {}
48
49 function Foo() {
50 - const $ = _c(2);
50 + const $ = _c(4);
51 const ref = useCustomRef();
52 let t0;
53 - if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
53 + if ($[0] !== ref) {
54 t0 = () => {
55 ref.current?.click();
56 };
57 - $[0] = t0;
57 + $[0] = ref;
58 + $[1] = t0;
59 } else {
59 - t0 = $[0];
60 + t0 = $[1];
61 }
62 const onClick = t0;
63 let t1;
63 - if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
64 + if ($[2] !== onClick) {
65 t1 = <button onClick={onClick} />;
65 - $[1] = t1;
66 + $[2] = onClick;
67 + $[3] = t1;
68 } else {
67 - t1 = $[1];
69 + t1 = $[3];
70 }
71 return t1;
72 }
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-like-name-in-useCallback-2.js
+1 -1
@@ -10,7 +10,7 @@ function Foo() {
10
11 const onClick = useCallback(() => {
12 ref.current?.click();
13 - }, []);
13 + }, [ref]);
14
15 return <button onClick={onClick} />;
16 }
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-like-name-in-useCallback.expect.md
+10 -8
@@ -14,7 +14,7 @@ function Foo() {
14
15 const onClick = useCallback(() => {
16 customRef.current?.click();
17 - }, []);
17 + }, [customRef]);
18
19 return <button onClick={onClick} />;
20 }
@@ -47,24 +47,26 @@ function useCustomRef() {
47 function _temp() {}
48
49 function Foo() {
50 - const $ = _c(2);
50 + const $ = _c(4);
51 const customRef = useCustomRef();
52 let t0;
53 - if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
53 + if ($[0] !== customRef) {
54 t0 = () => {
55 customRef.current?.click();
56 };
57 - $[0] = t0;
57 + $[0] = customRef;
58 + $[1] = t0;
59 } else {
59 - t0 = $[0];
60 + t0 = $[1];
61 }
62 const onClick = t0;
63 let t1;
63 - if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
64 + if ($[2] !== onClick) {
65 t1 = <button onClick={onClick} />;
65 - $[1] = t1;
66 + $[2] = onClick;
67 + $[3] = t1;
68 } else {
67 - t1 = $[1];
69 + t1 = $[3];
70 }
71 return t1;
72 }
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-like-name-in-useCallback.js
+1 -1
@@ -10,7 +10,7 @@ function Foo() {
10
11 const onClick = useCallback(() => {
12 customRef.current?.click();
13 - }, []);
13 + }, [customRef]);
14
15 return <button onClick={onClick} />;
16 }