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 ( + <> + + \ 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 ( + <> + + \ 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 ( + <> + + \ 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 ( + <> + + \ 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 ( + <> + +