diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts
index f9dfea52f363e..c02fe5af10301 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts
@@ -291,6 +291,10 @@ export type FunctionEffect =
kind: "GlobalMutation";
error: CompilerErrorDetailOptions;
}
+ | {
+ kind: "RefMutation";
+ error: CompilerErrorDetailOptions;
+ }
| {
kind: "ReactMutation";
error: CompilerErrorDetailOptions;
diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts
index 520684c026bda..cacd3e0eebc59 100644
--- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts
+++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts
@@ -29,6 +29,8 @@ import {
isArrayType,
isMutableEffect,
isObjectType,
+ isRefValueType,
+ isUseRefType,
} from "../HIR/HIR";
import { FunctionSignature } from "../HIR/ObjectShape";
import {
@@ -239,7 +241,8 @@ export default function inferReferenceEffects(
functionEffects.forEach((eff) => {
switch (eff.kind) {
case "ReactMutation":
- case "GlobalMutation": {
+ case "GlobalMutation":
+ case "RefMutation": {
CompilerError.throw(eff.error);
}
case "ContextMutation": {
@@ -407,7 +410,8 @@ class InferenceState {
for (const effect of value.loweredFunc.func.effects) {
if (
effect.kind === "GlobalMutation" ||
- effect.kind === "ReactMutation"
+ effect.kind === "ReactMutation" ||
+ effect.kind === "RefMutation"
) {
// Known effects are always propagated upwards
functionEffects.push(effect);
@@ -539,10 +543,12 @@ class InferenceState {
let reason = getWriteErrorReason(valueKind);
functionEffect = {
kind:
- valueKind.reason.size === 1 &&
- valueKind.reason.has(ValueReason.Global)
- ? "GlobalMutation"
- : "ReactMutation",
+ isRefValueType(place.identifier) || isUseRefType(place.identifier)
+ ? "RefMutation"
+ : valueKind.reason.size === 1 &&
+ valueKind.reason.has(ValueReason.Global)
+ ? "GlobalMutation"
+ : "ReactMutation",
error: {
reason,
description:
@@ -578,10 +584,12 @@ class InferenceState {
let reason = getWriteErrorReason(valueKind);
functionEffect = {
kind:
- valueKind.reason.size === 1 &&
- valueKind.reason.has(ValueReason.Global)
- ? "GlobalMutation"
- : "ReactMutation",
+ isRefValueType(place.identifier) || isUseRefType(place.identifier)
+ ? "RefMutation"
+ : valueKind.reason.size === 1 &&
+ valueKind.reason.has(ValueReason.Global)
+ ? "GlobalMutation"
+ : "ReactMutation",
error: {
reason,
description:
@@ -1139,7 +1147,9 @@ function inferBlock(
);
functionEffects.push(
...propEffects.filter(
- (propEffect) => propEffect.kind !== "GlobalMutation"
+ (propEffect) =>
+ propEffect.kind !== "GlobalMutation" &&
+ propEffect.kind !== "RefMutation"
)
);
}
@@ -1333,7 +1343,10 @@ function inferBlock(
functionEffects.push(
...argumentEffects.filter(
(argEffect) =>
- !isUseEffect || i !== 0 || argEffect.kind !== "GlobalMutation"
+ !isUseEffect ||
+ i !== 0 ||
+ (argEffect.kind !== "GlobalMutation" &&
+ argEffect.kind !== "RefMutation")
)
);
hasCaptureArgument ||= place.effect === Effect.Capture;
@@ -1462,7 +1475,10 @@ function inferBlock(
functionEffects.push(
...argumentEffects.filter(
(argEffect) =>
- !isUseEffect || i !== 0 || argEffect.kind !== "GlobalMutation"
+ !isUseEffect ||
+ i !== 0 ||
+ (argEffect.kind !== "GlobalMutation" &&
+ argEffect.kind !== "RefMutation")
)
);
hasCaptureArgument ||= place.effect === Effect.Capture;
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-in-callback-passed-to-jsx-indirect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-in-callback-passed-to-jsx-indirect.expect.md
new file mode 100644
index 0000000000000..12d80c162c3f2
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-in-callback-passed-to-jsx-indirect.expect.md
@@ -0,0 +1,108 @@
+
+## Input
+
+```javascript
+import { useRef } from "react";
+
+function Component() {
+ const ref = useRef(null);
+
+ const setRef = () => {
+ if (ref.current !== null) {
+ ref.current = "";
+ }
+ };
+
+ const onClick = () => {
+ setRef();
+ };
+
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{}],
+};
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime";
+import { useRef } from "react";
+
+function Component() {
+ const $ = _c(10);
+ const ref = useRef(null);
+ let t0;
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
+ t0 = () => {
+ if (ref.current !== null) {
+ ref.current = "";
+ }
+ };
+ $[0] = t0;
+ } else {
+ t0 = $[0];
+ }
+ const setRef = t0;
+ let t1;
+ if ($[1] !== setRef) {
+ t1 = () => {
+ setRef();
+ };
+ $[1] = setRef;
+ $[2] = t1;
+ } else {
+ t1 = $[2];
+ }
+ const onClick = t1;
+ let t2;
+ if ($[3] !== ref) {
+ t2 = ;
+ $[3] = ref;
+ $[4] = t2;
+ } else {
+ t2 = $[4];
+ }
+ let t3;
+ if ($[5] !== onClick) {
+ t3 = ;
+ $[5] = onClick;
+ $[6] = t3;
+ } else {
+ t3 = $[6];
+ }
+ let t4;
+ if ($[7] !== t2 || $[8] !== t3) {
+ t4 = (
+ <>
+ {t2}
+ {t3}
+ >
+ );
+ $[7] = t2;
+ $[8] = t3;
+ $[9] = t4;
+ } else {
+ t4 = $[9];
+ }
+ return t4;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{}],
+};
+
+```
+
+### Eval output
+(kind: ok)
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-in-callback-passed-to-jsx-indirect.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-in-callback-passed-to-jsx-indirect.tsx
new file mode 100644
index 0000000000000..8addd14792952
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-in-callback-passed-to-jsx-indirect.tsx
@@ -0,0 +1,27 @@
+import { useRef } from "react";
+
+function Component() {
+ const ref = useRef(null);
+
+ const setRef = () => {
+ if (ref.current !== null) {
+ ref.current = "";
+ }
+ };
+
+ const onClick = () => {
+ setRef();
+ };
+
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{}],
+};
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-in-callback-passed-to-jsx.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-in-callback-passed-to-jsx.expect.md
new file mode 100644
index 0000000000000..26a777c033255
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-in-callback-passed-to-jsx.expect.md
@@ -0,0 +1,93 @@
+
+## Input
+
+```javascript
+import { useRef } from "react";
+
+function Component() {
+ const ref = useRef(null);
+
+ const onClick = () => {
+ if (ref.current !== null) {
+ ref.current = "";
+ }
+ };
+
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{}],
+};
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime";
+import { useRef } from "react";
+
+function Component() {
+ const $ = _c(8);
+ const ref = useRef(null);
+ let t0;
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
+ t0 = () => {
+ if (ref.current !== null) {
+ ref.current = "";
+ }
+ };
+ $[0] = t0;
+ } else {
+ t0 = $[0];
+ }
+ const onClick = t0;
+ let t1;
+ if ($[1] !== ref) {
+ t1 = ;
+ $[1] = ref;
+ $[2] = t1;
+ } else {
+ t1 = $[2];
+ }
+ let t2;
+ if ($[3] !== onClick) {
+ t2 = ;
+ $[3] = onClick;
+ $[4] = t2;
+ } else {
+ t2 = $[4];
+ }
+ let t3;
+ if ($[5] !== t1 || $[6] !== t2) {
+ t3 = (
+ <>
+ {t1}
+ {t2}
+ >
+ );
+ $[5] = t1;
+ $[6] = t2;
+ $[7] = t3;
+ } else {
+ t3 = $[7];
+ }
+ return t3;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{}],
+};
+
+```
+
+### Eval output
+(kind: ok)
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-in-callback-passed-to-jsx.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-in-callback-passed-to-jsx.tsx
new file mode 100644
index 0000000000000..f3c9d5ab913b4
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-in-callback-passed-to-jsx.tsx
@@ -0,0 +1,23 @@
+import { useRef } from "react";
+
+function Component() {
+ const ref = useRef(null);
+
+ const onClick = () => {
+ if (ref.current !== null) {
+ ref.current = "";
+ }
+ };
+
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{}],
+};
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-property-in-callback-passed-to-jsx-indirect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-property-in-callback-passed-to-jsx-indirect.expect.md
new file mode 100644
index 0000000000000..bfd39bc445fd7
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-property-in-callback-passed-to-jsx-indirect.expect.md
@@ -0,0 +1,108 @@
+
+## Input
+
+```javascript
+import { useRef } from "react";
+
+function Component() {
+ const ref = useRef(null);
+
+ const setRef = () => {
+ if (ref.current !== null) {
+ ref.current.value = "";
+ }
+ };
+
+ const onClick = () => {
+ setRef();
+ };
+
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{}],
+};
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime";
+import { useRef } from "react";
+
+function Component() {
+ const $ = _c(10);
+ const ref = useRef(null);
+ let t0;
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
+ t0 = () => {
+ if (ref.current !== null) {
+ ref.current.value = "";
+ }
+ };
+ $[0] = t0;
+ } else {
+ t0 = $[0];
+ }
+ const setRef = t0;
+ let t1;
+ if ($[1] !== setRef) {
+ t1 = () => {
+ setRef();
+ };
+ $[1] = setRef;
+ $[2] = t1;
+ } else {
+ t1 = $[2];
+ }
+ const onClick = t1;
+ let t2;
+ if ($[3] !== ref) {
+ t2 = ;
+ $[3] = ref;
+ $[4] = t2;
+ } else {
+ t2 = $[4];
+ }
+ let t3;
+ if ($[5] !== onClick) {
+ t3 = ;
+ $[5] = onClick;
+ $[6] = t3;
+ } else {
+ t3 = $[6];
+ }
+ let t4;
+ if ($[7] !== t2 || $[8] !== t3) {
+ t4 = (
+ <>
+ {t2}
+ {t3}
+ >
+ );
+ $[7] = t2;
+ $[8] = t3;
+ $[9] = t4;
+ } else {
+ t4 = $[9];
+ }
+ return t4;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{}],
+};
+
+```
+
+### Eval output
+(kind: ok)
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-property-in-callback-passed-to-jsx-indirect.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-property-in-callback-passed-to-jsx-indirect.tsx
new file mode 100644
index 0000000000000..0f0f7a7e18cc9
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-property-in-callback-passed-to-jsx-indirect.tsx
@@ -0,0 +1,27 @@
+import { useRef } from "react";
+
+function Component() {
+ const ref = useRef(null);
+
+ const setRef = () => {
+ if (ref.current !== null) {
+ ref.current.value = "";
+ }
+ };
+
+ const onClick = () => {
+ setRef();
+ };
+
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{}],
+};
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-property-in-callback-passed-to-jsx.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-property-in-callback-passed-to-jsx.expect.md
new file mode 100644
index 0000000000000..b1fb5f77eef7b
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-property-in-callback-passed-to-jsx.expect.md
@@ -0,0 +1,93 @@
+
+## Input
+
+```javascript
+import { useRef } from "react";
+
+function Component() {
+ const ref = useRef(null);
+
+ const onClick = () => {
+ if (ref.current !== null) {
+ ref.current.value = "";
+ }
+ };
+
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{}],
+};
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime";
+import { useRef } from "react";
+
+function Component() {
+ const $ = _c(8);
+ const ref = useRef(null);
+ let t0;
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
+ t0 = () => {
+ if (ref.current !== null) {
+ ref.current.value = "";
+ }
+ };
+ $[0] = t0;
+ } else {
+ t0 = $[0];
+ }
+ const onClick = t0;
+ let t1;
+ if ($[1] !== ref) {
+ t1 = ;
+ $[1] = ref;
+ $[2] = t1;
+ } else {
+ t1 = $[2];
+ }
+ let t2;
+ if ($[3] !== onClick) {
+ t2 = ;
+ $[3] = onClick;
+ $[4] = t2;
+ } else {
+ t2 = $[4];
+ }
+ let t3;
+ if ($[5] !== t1 || $[6] !== t2) {
+ t3 = (
+ <>
+ {t1}
+ {t2}
+ >
+ );
+ $[5] = t1;
+ $[6] = t2;
+ $[7] = t3;
+ } else {
+ t3 = $[7];
+ }
+ return t3;
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{}],
+};
+
+```
+
+### Eval output
+(kind: ok)
\ No newline at end of file
diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-property-in-callback-passed-to-jsx.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-property-in-callback-passed-to-jsx.tsx
new file mode 100644
index 0000000000000..6b7e9dfba7b71
--- /dev/null
+++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-mutating-ref-property-in-callback-passed-to-jsx.tsx
@@ -0,0 +1,23 @@
+import { useRef } from "react";
+
+function Component() {
+ const ref = useRef(null);
+
+ const onClick = () => {
+ if (ref.current !== null) {
+ ref.current.value = "";
+ }
+ };
+
+ return (
+ <>
+
+
+ >
+ );
+}
+
+export const FIXTURE_ENTRYPOINT = {
+ fn: Component,
+ params: [{}],
+};