From 21f282425c751ee7926416642a0aded88d218623 Mon Sep 17 00:00:00 2001 From: Eliot Pontarelli <115111163+kolvian@users.noreply.github.com> Date: Fri, 14 Nov 2025 10:00:33 -0700 Subject: [PATCH] [compiler] Allow ref access in callbacks passed to event handler props (#35062) ## Summary Fixes #35040. The React compiler incorrectly flags ref access within event handlers as ref access at render time. For example, this code would fail to compile with error "Cannot access refs during render": ```tsx const onSubmit = async (data) => { const file = ref.current?.toFile(); // Incorrectly flagged as error };
``` This is a false positive because any built-in DOM event handler is guaranteed not to run at render time. This PR only supports built-in event handlers because there are no guarantees that user-made event handlers will not run at render time. ## How did you test this change? I created 4 test fixtures which validate this change: * allow-ref-access-in-event-handler-wrapper.tsx - Sync handler test input * allow-ref-access-in-event-handler-wrapper.expect.md - Sync handler expected output * allow-ref-access-in-async-event-handler-wrapper.tsx - Async handler test input * allow-ref-access-in-async-event-handler-wrapper.expect.md - Async handler expected output All linters and test suites also pass. --- .../src/HIR/Environment.ts | 9 ++ .../src/HIR/Globals.ts | 4 +- .../src/HIR/ObjectShape.ts | 18 ++- .../src/TypeInference/InferTypes.ts | 36 +++++ .../Validation/ValidateNoRefAccessInRender.ts | 33 ++-- ...s-in-async-event-handler-wrapper.expect.md | 148 ++++++++++++++++++ ...-access-in-async-event-handler-wrapper.tsx | 48 ++++++ ...-access-in-event-handler-wrapper.expect.md | 101 ++++++++++++ ...ow-ref-access-in-event-handler-wrapper.tsx | 36 +++++ ...-component-event-handler-wrapper.expect.md | 69 ++++++++ ...custom-component-event-handler-wrapper.tsx | 41 +++++ ...f-value-in-event-handler-wrapper.expect.md | 55 +++++++ ...ror.ref-value-in-event-handler-wrapper.tsx | 27 ++++ 13 files changed, 603 insertions(+), 22 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-async-event-handler-wrapper.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-async-event-handler-wrapper.tsx create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-event-handler-wrapper.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-event-handler-wrapper.tsx create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-value-in-custom-component-event-handler-wrapper.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-value-in-custom-component-event-handler-wrapper.tsx create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-value-in-event-handler-wrapper.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-value-in-event-handler-wrapper.tsx diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts index bb19eab93cf..a32823cb5f8 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -677,6 +677,15 @@ export const EnvironmentConfigSchema = z.object({ * from refs need to be stored in state during mount. */ enableAllowSetStateFromRefsInEffects: z.boolean().default(true), + + /** + * Enables inference of event handler types for JSX props on built-in DOM elements. + * When enabled, functions passed to event handler props (props starting with "on") + * on primitive JSX tags are inferred to have the BuiltinEventHandlerId type, which + * allows ref access within those functions since DOM event handlers are guaranteed + * by React to only execute in response to events, not during render. + */ + enableInferEventHandlers: z.boolean().default(false), }); export type EnvironmentConfig = z.infer; diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts index 561bdab6982..f42d6fc5bd9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts @@ -29,7 +29,7 @@ import { BuiltInUseTransitionId, BuiltInWeakMapId, BuiltInWeakSetId, - BuiltinEffectEventId, + BuiltInEffectEventId, ReanimatedSharedValueId, ShapeRegistry, addFunction, @@ -863,7 +863,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [ returnType: { kind: 'Function', return: {kind: 'Poly'}, - shapeId: BuiltinEffectEventId, + shapeId: BuiltInEffectEventId, isConstructor: false, }, calleeEffect: Effect.Read, diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts index beaff321e26..eb771615619 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts @@ -403,8 +403,9 @@ export const BuiltInStartTransitionId = 'BuiltInStartTransition'; export const BuiltInFireId = 'BuiltInFire'; export const BuiltInFireFunctionId = 'BuiltInFireFunction'; export const BuiltInUseEffectEventId = 'BuiltInUseEffectEvent'; -export const BuiltinEffectEventId = 'BuiltInEffectEventFunction'; +export const BuiltInEffectEventId = 'BuiltInEffectEventFunction'; export const BuiltInAutodepsId = 'BuiltInAutoDepsId'; +export const BuiltInEventHandlerId = 'BuiltInEventHandlerId'; // See getReanimatedModuleType() in Globals.ts — this is part of supporting Reanimated's ref-like types export const ReanimatedSharedValueId = 'ReanimatedSharedValueId'; @@ -1243,7 +1244,20 @@ addFunction( calleeEffect: Effect.ConditionallyMutate, returnValueKind: ValueKind.Mutable, }, - BuiltinEffectEventId, + BuiltInEffectEventId, +); + +addFunction( + BUILTIN_SHAPES, + [], + { + positionalParams: [], + restParam: Effect.ConditionallyMutate, + returnType: {kind: 'Poly'}, + calleeEffect: Effect.ConditionallyMutate, + returnValueKind: ValueKind.Mutable, + }, + BuiltInEventHandlerId, ); /** diff --git a/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts b/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts index 55974db14ce..b6ec11fdb4f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts @@ -25,6 +25,7 @@ import { } from '../HIR/HIR'; import { BuiltInArrayId, + BuiltInEventHandlerId, BuiltInFunctionId, BuiltInJsxId, BuiltInMixedReadonlyId, @@ -471,6 +472,41 @@ function* generateInstructionTypes( } } } + if (env.config.enableInferEventHandlers) { + if ( + value.kind === 'JsxExpression' && + value.tag.kind === 'BuiltinTag' && + !value.tag.name.includes('-') + ) { + /* + * Infer event handler types for built-in DOM elements. + * Props starting with "on" (e.g., onClick, onSubmit) on primitive tags + * are inferred as event handlers. This allows functions with ref access + * to be passed to these props, since DOM event handlers are guaranteed + * by React to only execute in response to events, never during render. + * + * We exclude tags with hyphens to avoid web components (custom elements), + * which are required by the HTML spec to contain a hyphen. Web components + * may call event handler props during their lifecycle methods (e.g., + * connectedCallback), which would be unsafe for ref access. + */ + for (const prop of value.props) { + if ( + prop.kind === 'JsxAttribute' && + prop.name.startsWith('on') && + prop.name.length > 2 && + prop.name[2] === prop.name[2].toUpperCase() + ) { + yield equation(prop.place.identifier.type, { + kind: 'Function', + shapeId: BuiltInEventHandlerId, + return: makeType(), + isConstructor: false, + }); + } + } + } + } yield equation(left, {kind: 'Object', shapeId: BuiltInJsxId}); break; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts index abbb7d84769..232e9f55bbc 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts @@ -14,12 +14,14 @@ import { BlockId, HIRFunction, IdentifierId, + Identifier, Place, SourceLocation, getHookKindForType, isRefValueType, isUseRefType, } from '../HIR'; +import {BuiltInEventHandlerId} from '../HIR/ObjectShape'; import { eachInstructionOperand, eachInstructionValueOperand, @@ -183,6 +185,11 @@ function refTypeOfType(place: Place): RefAccessType { } } +function isEventHandlerType(identifier: Identifier): boolean { + const type = identifier.type; + return type.kind === 'Function' && type.shapeId === BuiltInEventHandlerId; +} + function tyEqual(a: RefAccessType, b: RefAccessType): boolean { if (a.kind !== b.kind) { return false; @@ -519,6 +526,9 @@ function validateNoRefAccessInRenderImpl( */ if (!didError) { const isRefLValue = isUseRefType(instr.lvalue.identifier); + const isEventHandlerLValue = isEventHandlerType( + instr.lvalue.identifier, + ); for (const operand of eachInstructionValueOperand(instr.value)) { /** * By default we check that function call operands are not refs, @@ -526,29 +536,16 @@ function validateNoRefAccessInRenderImpl( */ if ( isRefLValue || + isEventHandlerLValue || (hookKind != null && hookKind !== 'useState' && hookKind !== 'useReducer') ) { /** - * Special cases: - * - * 1. the lvalue is a ref - * In general passing a ref to a function may access that ref - * value during render, so we disallow it. - * - * The main exception is the "mergeRefs" pattern, ie a function - * that accepts multiple refs as arguments (or an array of refs) - * and returns a new, aggregated ref. If the lvalue is a ref, - * we assume that the user is doing this pattern and allow passing - * refs. - * - * Eg `const mergedRef = mergeRefs(ref1, ref2)` - * - * 2. calling hooks - * - * Hooks are independently checked to ensure they don't access refs - * during render. + * Allow passing refs or ref-accessing functions when: + * 1. lvalue is a ref (mergeRefs pattern: `mergeRefs(ref1, ref2)`) + * 2. lvalue is an event handler (DOM events execute outside render) + * 3. calling hooks (independently validated for ref safety) */ validateNoDirectRefValueAccess(errors, operand, env); } else if (interpolatedAsJsx.has(instr.lvalue.identifier.id)) { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-async-event-handler-wrapper.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-async-event-handler-wrapper.expect.md new file mode 100644 index 00000000000..5cee9a68ceb --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-async-event-handler-wrapper.expect.md @@ -0,0 +1,148 @@ + +## Input + +```javascript +// @enableInferEventHandlers +import {useRef} from 'react'; + +// Simulates react-hook-form's handleSubmit +function handleSubmit(callback: (data: T) => void | Promise) { + return (event: any) => { + event.preventDefault(); + callback({} as T); + }; +} + +// Simulates an upload function +async function upload(file: any): Promise<{blob: {url: string}}> { + return {blob: {url: 'https://example.com/file.jpg'}}; +} + +interface SignatureRef { + toFile(): any; +} + +function Component() { + const ref = useRef(null); + + const onSubmit = async (value: any) => { + // This should be allowed: accessing ref.current in an async event handler + // that's wrapped and passed to onSubmit prop + let sigUrl: string; + if (value.hasSignature) { + const {blob} = await upload(ref.current?.toFile()); + sigUrl = blob?.url || ''; + } else { + sigUrl = value.signature; + } + console.log('Signature URL:', sigUrl); + }; + + return ( + + + + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableInferEventHandlers +import { useRef } from "react"; + +// Simulates react-hook-form's handleSubmit +function handleSubmit(callback) { + const $ = _c(2); + let t0; + if ($[0] !== callback) { + t0 = (event) => { + event.preventDefault(); + callback({} as T); + }; + $[0] = callback; + $[1] = t0; + } else { + t0 = $[1]; + } + return t0; +} + +// Simulates an upload function +async function upload(file) { + const $ = _c(1); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = { blob: { url: "https://example.com/file.jpg" } }; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} + +interface SignatureRef { + toFile(): any; +} + +function Component() { + const $ = _c(4); + const ref = useRef(null); + + const onSubmit = async (value) => { + let sigUrl; + if (value.hasSignature) { + const { blob } = await upload(ref.current?.toFile()); + sigUrl = blob?.url || ""; + } else { + sigUrl = value.signature; + } + + console.log("Signature URL:", sigUrl); + }; + + const t0 = handleSubmit(onSubmit); + let t1; + let t2; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t1 = ; + t2 = ; + $[0] = t1; + $[1] = t2; + } else { + t1 = $[0]; + t2 = $[1]; + } + let t3; + if ($[2] !== t0) { + t3 = ( +
+ {t1} + {t2} +
+ ); + $[2] = t0; + $[3] = t3; + } else { + t3 = $[3]; + } + 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-ref-access-in-async-event-handler-wrapper.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-async-event-handler-wrapper.tsx new file mode 100644 index 00000000000..be6f6656e18 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-async-event-handler-wrapper.tsx @@ -0,0 +1,48 @@ +// @enableInferEventHandlers +import {useRef} from 'react'; + +// Simulates react-hook-form's handleSubmit +function handleSubmit(callback: (data: T) => void | Promise) { + return (event: any) => { + event.preventDefault(); + callback({} as T); + }; +} + +// Simulates an upload function +async function upload(file: any): Promise<{blob: {url: string}}> { + return {blob: {url: 'https://example.com/file.jpg'}}; +} + +interface SignatureRef { + toFile(): any; +} + +function Component() { + const ref = useRef(null); + + const onSubmit = async (value: any) => { + // This should be allowed: accessing ref.current in an async event handler + // that's wrapped and passed to onSubmit prop + let sigUrl: string; + if (value.hasSignature) { + const {blob} = await upload(ref.current?.toFile()); + sigUrl = blob?.url || ''; + } else { + sigUrl = value.signature; + } + console.log('Signature URL:', sigUrl); + }; + + return ( +
+ + +
+ ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-event-handler-wrapper.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-event-handler-wrapper.expect.md new file mode 100644 index 00000000000..d40a1e080ad --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-event-handler-wrapper.expect.md @@ -0,0 +1,101 @@ + +## Input + +```javascript +// @enableInferEventHandlers +import {useRef} from 'react'; + +// Simulates react-hook-form's handleSubmit or similar event handler wrappers +function handleSubmit(callback: (data: T) => void) { + return (event: any) => { + event.preventDefault(); + callback({} as T); + }; +} + +function Component() { + const ref = useRef(null); + + const onSubmit = (data: any) => { + // This should be allowed: accessing ref.current in an event handler + // that's wrapped by handleSubmit and passed to onSubmit prop + if (ref.current !== null) { + console.log(ref.current.value); + } + }; + + return ( + <> + +
+ +
+ + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableInferEventHandlers +import { useRef } from "react"; + +// Simulates react-hook-form's handleSubmit or similar event handler wrappers +function handleSubmit(callback) { + const $ = _c(2); + let t0; + if ($[0] !== callback) { + t0 = (event) => { + event.preventDefault(); + callback({} as T); + }; + $[0] = callback; + $[1] = t0; + } else { + t0 = $[1]; + } + return t0; +} + +function Component() { + const $ = _c(1); + const ref = useRef(null); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + const onSubmit = (data) => { + if (ref.current !== null) { + console.log(ref.current.value); + } + }; + + t0 = ( + <> + +
+ +
+ + ); + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} + +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-ref-access-in-event-handler-wrapper.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-event-handler-wrapper.tsx new file mode 100644 index 00000000000..f305a1f9ac6 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-event-handler-wrapper.tsx @@ -0,0 +1,36 @@ +// @enableInferEventHandlers +import {useRef} from 'react'; + +// Simulates react-hook-form's handleSubmit or similar event handler wrappers +function handleSubmit(callback: (data: T) => void) { + return (event: any) => { + event.preventDefault(); + callback({} as T); + }; +} + +function Component() { + const ref = useRef(null); + + const onSubmit = (data: any) => { + // This should be allowed: accessing ref.current in an event handler + // that's wrapped by handleSubmit and passed to onSubmit prop + if (ref.current !== null) { + console.log(ref.current.value); + } + }; + + return ( + <> + +
+ +
+ + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-value-in-custom-component-event-handler-wrapper.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-value-in-custom-component-event-handler-wrapper.expect.md new file mode 100644 index 00000000000..2a3657eda34 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-value-in-custom-component-event-handler-wrapper.expect.md @@ -0,0 +1,69 @@ + +## Input + +```javascript +// @enableInferEventHandlers +import {useRef} from 'react'; + +// Simulates a custom component wrapper +function CustomForm({onSubmit, children}: any) { + return
{children}
; +} + +// Simulates react-hook-form's handleSubmit +function handleSubmit(callback: (data: T) => void) { + return (event: any) => { + event.preventDefault(); + callback({} as T); + }; +} + +function Component() { + const ref = useRef(null); + + const onSubmit = (data: any) => { + // This should error: passing function with ref access to custom component + // event handler, even though it would be safe on a native
+ if (ref.current !== null) { + console.log(ref.current.value); + } + }; + + return ( + <> + + + + + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], +}; + +``` + + +## Error + +``` +Found 1 error: + +Error: Cannot access refs during render + +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). + +error.ref-value-in-custom-component-event-handler-wrapper.ts:31:41 + 29 | <> + 30 | +> 31 | + | ^^^^^^^^ Passing a ref to a function may read its value during render + 32 | + 33 | + 34 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-value-in-custom-component-event-handler-wrapper.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-value-in-custom-component-event-handler-wrapper.tsx new file mode 100644 index 00000000000..b90a1217165 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-value-in-custom-component-event-handler-wrapper.tsx @@ -0,0 +1,41 @@ +// @enableInferEventHandlers +import {useRef} from 'react'; + +// Simulates a custom component wrapper +function CustomForm({onSubmit, children}: any) { + return {children}
; +} + +// Simulates react-hook-form's handleSubmit +function handleSubmit(callback: (data: T) => void) { + return (event: any) => { + event.preventDefault(); + callback({} as T); + }; +} + +function Component() { + const ref = useRef(null); + + const onSubmit = (data: any) => { + // This should error: passing function with ref access to custom component + // event handler, even though it would be safe on a native
+ if (ref.current !== null) { + console.log(ref.current.value); + } + }; + + return ( + <> + + + + + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-value-in-event-handler-wrapper.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-value-in-event-handler-wrapper.expect.md new file mode 100644 index 00000000000..718e2c81419 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-value-in-event-handler-wrapper.expect.md @@ -0,0 +1,55 @@ + +## Input + +```javascript +// @enableInferEventHandlers +import {useRef} from 'react'; + +// Simulates a handler wrapper +function handleClick(value: any) { + return () => { + console.log(value); + }; +} + +function Component() { + const ref = useRef(null); + + // This should still error: passing ref.current directly to a wrapper + // The ref value is accessed during render, not in the event handler + return ( + <> + + + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], +}; + +``` + + +## Error + +``` +Found 1 error: + +Error: Cannot access refs during render + +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). + +error.ref-value-in-event-handler-wrapper.ts:19:35 + 17 | <> + 18 | +> 19 | + | ^^^^^^^^^^^ Cannot access ref value during render + 20 | + 21 | ); + 22 | } +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-value-in-event-handler-wrapper.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-value-in-event-handler-wrapper.tsx new file mode 100644 index 00000000000..58313e560ce --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-value-in-event-handler-wrapper.tsx @@ -0,0 +1,27 @@ +// @enableInferEventHandlers +import {useRef} from 'react'; + +// Simulates a handler wrapper +function handleClick(value: any) { + return () => { + console.log(value); + }; +} + +function Component() { + const ref = useRef(null); + + // This should still error: passing ref.current directly to a wrapper + // The ref value is accessed during render, not in the event handler + return ( + <> + + + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{}], +};