@samitouri / QOS-React-2 / commits / 827cbea417

compiler: Add support for ref effects

Fixes false positives where we currently disallow mutations of refs from callbacks passed to JSX, if the ref is also passed to jsx. We consider these to be mutations of "frozen" values, but refs are explicitly allowed to have interior mutability. The fix is to always allow (at leat within InferReferenceEffects) for refs to be mutated. This means we completely rely on ValidateNoRefAccessInRender to validate ref access and stop reporting false positives. ghstack-source-id: 1a30609f5f7831086077d10dac46ab70687f47e0 Pull Request resolved: https://github.com/facebook/react/pull/29733

Joe Savona committed Jun 6, 2024 at 17:02 UTC 827cbea417a4058ce544184adff2ee2014625309
13 files changed +606 -2
compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts
+14 -2
@@ -29,6 +29,8 @@ import {
29 isArrayType,
30 isMutableEffect,
31 isObjectType,
32 + isRefValueType,
33 + isUseRefType,
34 } from "../HIR/HIR";
35 import { FunctionSignature } from "../HIR/ObjectShape";
36 import {
@@ -521,7 +523,12 @@ class InferenceState {
523 break;
524 }
525 case Effect.Mutate: {
524 - if (valueKind.kind === ValueKind.Context) {
526 + if (
527 + isRefValueType(place.identifier) ||
528 + isUseRefType(place.identifier)
529 + ) {
530 + // no-op: refs are validate via ValidateNoRefAccessInRender
531 + } else if (valueKind.kind === ValueKind.Context) {
532 functionEffect = {
533 kind: "ContextMutation",
534 loc: place.loc,
@@ -560,7 +567,12 @@ class InferenceState {
567 break;
568 }
569 case Effect.Store: {
563 - if (valueKind.kind === ValueKind.Context) {
570 + if (
571 + isRefValueType(place.identifier) ||
572 + isUseRefType(place.identifier)
573 + ) {
574 + // no-op: refs are validate via ValidateNoRefAccessInRender
575 + } else if (valueKind.kind === ValueKind.Context) {
576 functionEffect = {
577 kind: "ContextMutation",
578 loc: place.loc,
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-in-callback-passed-to-jsx-indirect.expect.md new
+109
@@ -0,0 +1,109 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateRefAccessDuringRender
6 +import { useRef } from "react";
7 +
8 +function Component() {
9 + const ref = useRef(null);
10 +
11 + const setRef = () => {
12 + if (ref.current !== null) {
13 + ref.current = "";
14 + }
15 + };
16 +
17 + const onClick = () => {
18 + setRef();
19 + };
20 +
21 + return (
22 + <>
23 + <input ref={ref} />
24 + <button onClick={onClick} />
25 + </>
26 + );
27 +}
28 +
29 +export const FIXTURE_ENTRYPOINT = {
30 + fn: Component,
31 + params: [{}],
32 +};
33 +
34 +```
35 +
36 +## Code
37 +
38 +```javascript
39 +import { c as _c } from "react/compiler-runtime"; // @validateRefAccessDuringRender
40 +import { useRef } from "react";
41 +
42 +function Component() {
43 + const $ = _c(10);
44 + const ref = useRef(null);
45 + let t0;
46 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
47 + t0 = () => {
48 + if (ref.current !== null) {
49 + ref.current = "";
50 + }
51 + };
52 + $[0] = t0;
53 + } else {
54 + t0 = $[0];
55 + }
56 + const setRef = t0;
57 + let t1;
58 + if ($[1] !== setRef) {
59 + t1 = () => {
60 + setRef();
61 + };
62 + $[1] = setRef;
63 + $[2] = t1;
64 + } else {
65 + t1 = $[2];
66 + }
67 + const onClick = t1;
68 + let t2;
69 + if ($[3] !== ref) {
70 + t2 = <input ref={ref} />;
71 + $[3] = ref;
72 + $[4] = t2;
73 + } else {
74 + t2 = $[4];
75 + }
76 + let t3;
77 + if ($[5] !== onClick) {
78 + t3 = <button onClick={onClick} />;
79 + $[5] = onClick;
80 + $[6] = t3;
81 + } else {
82 + t3 = $[6];
83 + }
84 + let t4;
85 + if ($[7] !== t2 || $[8] !== t3) {
86 + t4 = (
87 + <>
88 + {t2}
89 + {t3}
90 + </>
91 + );
92 + $[7] = t2;
93 + $[8] = t3;
94 + $[9] = t4;
95 + } else {
96 + t4 = $[9];
97 + }
98 + return t4;
99 +}
100 +
101 +export const FIXTURE_ENTRYPOINT = {
102 + fn: Component,
103 + params: [{}],
104 +};
105 +
106 +```
107 +
108 +### Eval output
109 +(kind: ok) <input><button></button>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-in-callback-passed-to-jsx-indirect.tsx new
+28
@@ -0,0 +1,28 @@
1 +// @validateRefAccessDuringRender
2 +import { useRef } from "react";
3 +
4 +function Component() {
5 + const ref = useRef(null);
6 +
7 + const setRef = () => {
8 + if (ref.current !== null) {
9 + ref.current = "";
10 + }
11 + };
12 +
13 + const onClick = () => {
14 + setRef();
15 + };
16 +
17 + return (
18 + <>
19 + <input ref={ref} />
20 + <button onClick={onClick} />
21 + </>
22 + );
23 +}
24 +
25 +export const FIXTURE_ENTRYPOINT = {
26 + fn: Component,
27 + params: [{}],
28 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-in-callback-passed-to-jsx.expect.md new
+94
@@ -0,0 +1,94 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateRefAccessDuringRender
6 +import { useRef } from "react";
7 +
8 +function Component() {
9 + const ref = useRef(null);
10 +
11 + const onClick = () => {
12 + if (ref.current !== null) {
13 + ref.current = "";
14 + }
15 + };
16 +
17 + return (
18 + <>
19 + <input ref={ref} />
20 + <button onClick={onClick} />
21 + </>
22 + );
23 +}
24 +
25 +export const FIXTURE_ENTRYPOINT = {
26 + fn: Component,
27 + params: [{}],
28 +};
29 +
30 +```
31 +
32 +## Code
33 +
34 +```javascript
35 +import { c as _c } from "react/compiler-runtime"; // @validateRefAccessDuringRender
36 +import { useRef } from "react";
37 +
38 +function Component() {
39 + const $ = _c(8);
40 + const ref = useRef(null);
41 + let t0;
42 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
43 + t0 = () => {
44 + if (ref.current !== null) {
45 + ref.current = "";
46 + }
47 + };
48 + $[0] = t0;
49 + } else {
50 + t0 = $[0];
51 + }
52 + const onClick = t0;
53 + let t1;
54 + if ($[1] !== ref) {
55 + t1 = <input ref={ref} />;
56 + $[1] = ref;
57 + $[2] = t1;
58 + } else {
59 + t1 = $[2];
60 + }
61 + let t2;
62 + if ($[3] !== onClick) {
63 + t2 = <button onClick={onClick} />;
64 + $[3] = onClick;
65 + $[4] = t2;
66 + } else {
67 + t2 = $[4];
68 + }
69 + let t3;
70 + if ($[5] !== t1 || $[6] !== t2) {
71 + t3 = (
72 + <>
73 + {t1}
74 + {t2}
75 + </>
76 + );
77 + $[5] = t1;
78 + $[6] = t2;
79 + $[7] = t3;
80 + } else {
81 + t3 = $[7];
82 + }
83 + return t3;
84 +}
85 +
86 +export const FIXTURE_ENTRYPOINT = {
87 + fn: Component,
88 + params: [{}],
89 +};
90 +
91 +```
92 +
93 +### Eval output
94 +(kind: ok) <input><button></button>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-in-callback-passed-to-jsx.tsx new
+24
@@ -0,0 +1,24 @@
1 +// @validateRefAccessDuringRender
2 +import { useRef } from "react";
3 +
4 +function Component() {
5 + const ref = useRef(null);
6 +
7 + const onClick = () => {
8 + if (ref.current !== null) {
9 + ref.current = "";
10 + }
11 + };
12 +
13 + return (
14 + <>
15 + <input ref={ref} />
16 + <button onClick={onClick} />
17 + </>
18 + );
19 +}
20 +
21 +export const FIXTURE_ENTRYPOINT = {
22 + fn: Component,
23 + params: [{}],
24 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-property-in-callback-passed-to-jsx-indirect.expect.md new
+109
@@ -0,0 +1,109 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateRefAccessDuringRender
6 +import { useRef } from "react";
7 +
8 +function Component() {
9 + const ref = useRef(null);
10 +
11 + const setRef = () => {
12 + if (ref.current !== null) {
13 + ref.current.value = "";
14 + }
15 + };
16 +
17 + const onClick = () => {
18 + setRef();
19 + };
20 +
21 + return (
22 + <>
23 + <input ref={ref} />
24 + <button onClick={onClick} />
25 + </>
26 + );
27 +}
28 +
29 +export const FIXTURE_ENTRYPOINT = {
30 + fn: Component,
31 + params: [{}],
32 +};
33 +
34 +```
35 +
36 +## Code
37 +
38 +```javascript
39 +import { c as _c } from "react/compiler-runtime"; // @validateRefAccessDuringRender
40 +import { useRef } from "react";
41 +
42 +function Component() {
43 + const $ = _c(10);
44 + const ref = useRef(null);
45 + let t0;
46 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
47 + t0 = () => {
48 + if (ref.current !== null) {
49 + ref.current.value = "";
50 + }
51 + };
52 + $[0] = t0;
53 + } else {
54 + t0 = $[0];
55 + }
56 + const setRef = t0;
57 + let t1;
58 + if ($[1] !== setRef) {
59 + t1 = () => {
60 + setRef();
61 + };
62 + $[1] = setRef;
63 + $[2] = t1;
64 + } else {
65 + t1 = $[2];
66 + }
67 + const onClick = t1;
68 + let t2;
69 + if ($[3] !== ref) {
70 + t2 = <input ref={ref} />;
71 + $[3] = ref;
72 + $[4] = t2;
73 + } else {
74 + t2 = $[4];
75 + }
76 + let t3;
77 + if ($[5] !== onClick) {
78 + t3 = <button onClick={onClick} />;
79 + $[5] = onClick;
80 + $[6] = t3;
81 + } else {
82 + t3 = $[6];
83 + }
84 + let t4;
85 + if ($[7] !== t2 || $[8] !== t3) {
86 + t4 = (
87 + <>
88 + {t2}
89 + {t3}
90 + </>
91 + );
92 + $[7] = t2;
93 + $[8] = t3;
94 + $[9] = t4;
95 + } else {
96 + t4 = $[9];
97 + }
98 + return t4;
99 +}
100 +
101 +export const FIXTURE_ENTRYPOINT = {
102 + fn: Component,
103 + params: [{}],
104 +};
105 +
106 +```
107 +
108 +### Eval output
109 +(kind: ok) <input><button></button>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-property-in-callback-passed-to-jsx-indirect.tsx new
+28
@@ -0,0 +1,28 @@
1 +// @validateRefAccessDuringRender
2 +import { useRef } from "react";
3 +
4 +function Component() {
5 + const ref = useRef(null);
6 +
7 + const setRef = () => {
8 + if (ref.current !== null) {
9 + ref.current.value = "";
10 + }
11 + };
12 +
13 + const onClick = () => {
14 + setRef();
15 + };
16 +
17 + return (
18 + <>
19 + <input ref={ref} />
20 + <button onClick={onClick} />
21 + </>
22 + );
23 +}
24 +
25 +export const FIXTURE_ENTRYPOINT = {
26 + fn: Component,
27 + params: [{}],
28 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-property-in-callback-passed-to-jsx.expect.md new
+94
@@ -0,0 +1,94 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateRefAccessDuringRender
6 +import { useRef } from "react";
7 +
8 +function Component() {
9 + const ref = useRef(null);
10 +
11 + const onClick = () => {
12 + if (ref.current !== null) {
13 + ref.current.value = "";
14 + }
15 + };
16 +
17 + return (
18 + <>
19 + <input ref={ref} />
20 + <button onClick={onClick} />
21 + </>
22 + );
23 +}
24 +
25 +export const FIXTURE_ENTRYPOINT = {
26 + fn: Component,
27 + params: [{}],
28 +};
29 +
30 +```
31 +
32 +## Code
33 +
34 +```javascript
35 +import { c as _c } from "react/compiler-runtime"; // @validateRefAccessDuringRender
36 +import { useRef } from "react";
37 +
38 +function Component() {
39 + const $ = _c(8);
40 + const ref = useRef(null);
41 + let t0;
42 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
43 + t0 = () => {
44 + if (ref.current !== null) {
45 + ref.current.value = "";
46 + }
47 + };
48 + $[0] = t0;
49 + } else {
50 + t0 = $[0];
51 + }
52 + const onClick = t0;
53 + let t1;
54 + if ($[1] !== ref) {
55 + t1 = <input ref={ref} />;
56 + $[1] = ref;
57 + $[2] = t1;
58 + } else {
59 + t1 = $[2];
60 + }
61 + let t2;
62 + if ($[3] !== onClick) {
63 + t2 = <button onClick={onClick} />;
64 + $[3] = onClick;
65 + $[4] = t2;
66 + } else {
67 + t2 = $[4];
68 + }
69 + let t3;
70 + if ($[5] !== t1 || $[6] !== t2) {
71 + t3 = (
72 + <>
73 + {t1}
74 + {t2}
75 + </>
76 + );
77 + $[5] = t1;
78 + $[6] = t2;
79 + $[7] = t3;
80 + } else {
81 + t3 = $[7];
82 + }
83 + return t3;
84 +}
85 +
86 +export const FIXTURE_ENTRYPOINT = {
87 + fn: Component,
88 + params: [{}],
89 +};
90 +
91 +```
92 +
93 +### Eval output
94 +(kind: ok) <input><button></button>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-property-in-callback-passed-to-jsx.tsx new
+24
@@ -0,0 +1,24 @@
1 +// @validateRefAccessDuringRender
2 +import { useRef } from "react";
3 +
4 +function Component() {
5 + const ref = useRef(null);
6 +
7 + const onClick = () => {
8 + if (ref.current !== null) {
9 + ref.current.value = "";
10 + }
11 + };
12 +
13 + return (
14 + <>
15 + <input ref={ref} />
16 + <button onClick={onClick} />
17 + </>
18 + );
19 +}
20 +
21 +export const FIXTURE_ENTRYPOINT = {
22 + fn: Component,
23 + params: [{}],
24 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-ref-in-render.expect.md new
+28
@@ -0,0 +1,28 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateRefAccessDuringRender
6 +function Component() {
7 + const ref = useRef(null);
8 + ref.current = false;
9 +
10 + return <button ref={ref} />;
11 +}
12 +
13 +```
14 +
15 +
16 +## Error
17 +
18 +```
19 + 2 | function Component() {
20 + 3 | const ref = useRef(null);
21 +> 4 | ref.current = false;
22 + | ^^^^^^^^^^^ InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (4:4)
23 + 5 |
24 + 6 | return <button ref={ref} />;
25 + 7 | }
26 +```
27 +
28 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-ref-in-render.js new
+7
@@ -0,0 +1,7 @@
1 +// @validateRefAccessDuringRender
2 +function Component() {
3 + const ref = useRef(null);
4 + ref.current = false;
5 +
6 + return <button ref={ref} />;
7 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-refs-in-render-transitive.expect.md new
+35
@@ -0,0 +1,35 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateRefAccessDuringRender
6 +function Component() {
7 + const ref = useRef(null);
8 +
9 + const setRef = () => {
10 + ref.current = false;
11 + };
12 + const changeRef = setRef;
13 + changeRef();
14 +
15 + return <button ref={ref} />;
16 +}
17 +
18 +```
19 +
20 +
21 +## Error
22 +
23 +```
24 + 7 | };
25 + 8 | const changeRef = setRef;
26 +> 9 | changeRef();
27 + | ^^^^^^^^^ InvalidReact: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef). Function mutate? $39[11:13]:TObject<BuiltInFunction> accesses a ref (9:9)
28 +
29 +InvalidReact: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) (9:9)
30 + 10 |
31 + 11 | return <button ref={ref} />;
32 + 12 | }
33 +```
34 +
35 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-refs-in-render-transitive.js new
+12
@@ -0,0 +1,12 @@
1 +// @validateRefAccessDuringRender
2 +function Component() {
3 + const ref = useRef(null);
4 +
5 + const setRef = () => {
6 + ref.current = false;
7 + };
8 + const changeRef = setRef;
9 + changeRef();
10 +
11 + return <button ref={ref} />;
12 +}