@samitouri / QOS-React-2 / commits / 21f282425c

[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 }; <form onSubmit={handleSubmit(onSubmit)}> ``` 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.

Eliot Pontarelli committed Nov 14, 2025 at 10:00 UTC 21f282425c751ee7926416642a0aded88d218623
13 files changed +603 -22
compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts
+9
@@ -677,6 +677,15 @@ export const EnvironmentConfigSchema = z.object({
677 * from refs need to be stored in state during mount.
678 */
679 enableAllowSetStateFromRefsInEffects: z.boolean().default(true),
680 +
681 + /**
682 + * Enables inference of event handler types for JSX props on built-in DOM elements.
683 + * When enabled, functions passed to event handler props (props starting with "on")
684 + * on primitive JSX tags are inferred to have the BuiltinEventHandlerId type, which
685 + * allows ref access within those functions since DOM event handlers are guaranteed
686 + * by React to only execute in response to events, not during render.
687 + */
688 + enableInferEventHandlers: z.boolean().default(false),
689 });
690
691 export type EnvironmentConfig = z.infer<typeof EnvironmentConfigSchema>;
compiler/packages/babel-plugin-react-compiler/src/HIR/Globals.ts
+2 -2
@@ -29,7 +29,7 @@ import {
29 BuiltInUseTransitionId,
30 BuiltInWeakMapId,
31 BuiltInWeakSetId,
32 - BuiltinEffectEventId,
32 + BuiltInEffectEventId,
33 ReanimatedSharedValueId,
34 ShapeRegistry,
35 addFunction,
@@ -863,7 +863,7 @@ const REACT_APIS: Array<[string, BuiltInType]> = [
863 returnType: {
864 kind: 'Function',
865 return: {kind: 'Poly'},
866 - shapeId: BuiltinEffectEventId,
866 + shapeId: BuiltInEffectEventId,
867 isConstructor: false,
868 },
869 calleeEffect: Effect.Read,
compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts
+16 -2
@@ -403,8 +403,9 @@ export const BuiltInStartTransitionId = 'BuiltInStartTransition';
403 export const BuiltInFireId = 'BuiltInFire';
404 export const BuiltInFireFunctionId = 'BuiltInFireFunction';
405 export const BuiltInUseEffectEventId = 'BuiltInUseEffectEvent';
406 -export const BuiltinEffectEventId = 'BuiltInEffectEventFunction';
406 +export const BuiltInEffectEventId = 'BuiltInEffectEventFunction';
407 export const BuiltInAutodepsId = 'BuiltInAutoDepsId';
408 +export const BuiltInEventHandlerId = 'BuiltInEventHandlerId';
409
410 // See getReanimatedModuleType() in Globals.ts — this is part of supporting Reanimated's ref-like types
411 export const ReanimatedSharedValueId = 'ReanimatedSharedValueId';
@@ -1243,7 +1244,20 @@ addFunction(
1244 calleeEffect: Effect.ConditionallyMutate,
1245 returnValueKind: ValueKind.Mutable,
1246 },
1246 - BuiltinEffectEventId,
1247 + BuiltInEffectEventId,
1248 +);
1249 +
1250 +addFunction(
1251 + BUILTIN_SHAPES,
1252 + [],
1253 + {
1254 + positionalParams: [],
1255 + restParam: Effect.ConditionallyMutate,
1256 + returnType: {kind: 'Poly'},
1257 + calleeEffect: Effect.ConditionallyMutate,
1258 + returnValueKind: ValueKind.Mutable,
1259 + },
1260 + BuiltInEventHandlerId,
1261 );
1262
1263 /**
compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts
+36
@@ -25,6 +25,7 @@ import {
25 } from '../HIR/HIR';
26 import {
27 BuiltInArrayId,
28 + BuiltInEventHandlerId,
29 BuiltInFunctionId,
30 BuiltInJsxId,
31 BuiltInMixedReadonlyId,
@@ -471,6 +472,41 @@ function* generateInstructionTypes(
472 }
473 }
474 }
475 + if (env.config.enableInferEventHandlers) {
476 + if (
477 + value.kind === 'JsxExpression' &&
478 + value.tag.kind === 'BuiltinTag' &&
479 + !value.tag.name.includes('-')
480 + ) {
481 + /*
482 + * Infer event handler types for built-in DOM elements.
483 + * Props starting with "on" (e.g., onClick, onSubmit) on primitive tags
484 + * are inferred as event handlers. This allows functions with ref access
485 + * to be passed to these props, since DOM event handlers are guaranteed
486 + * by React to only execute in response to events, never during render.
487 + *
488 + * We exclude tags with hyphens to avoid web components (custom elements),
489 + * which are required by the HTML spec to contain a hyphen. Web components
490 + * may call event handler props during their lifecycle methods (e.g.,
491 + * connectedCallback), which would be unsafe for ref access.
492 + */
493 + for (const prop of value.props) {
494 + if (
495 + prop.kind === 'JsxAttribute' &&
496 + prop.name.startsWith('on') &&
497 + prop.name.length > 2 &&
498 + prop.name[2] === prop.name[2].toUpperCase()
499 + ) {
500 + yield equation(prop.place.identifier.type, {
501 + kind: 'Function',
502 + shapeId: BuiltInEventHandlerId,
503 + return: makeType(),
504 + isConstructor: false,
505 + });
506 + }
507 + }
508 + }
509 + }
510 yield equation(left, {kind: 'Object', shapeId: BuiltInJsxId});
511 break;
512 }
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts
+15 -18
@@ -14,12 +14,14 @@ import {
14 BlockId,
15 HIRFunction,
16 IdentifierId,
17 + Identifier,
18 Place,
19 SourceLocation,
20 getHookKindForType,
21 isRefValueType,
22 isUseRefType,
23 } from '../HIR';
24 +import {BuiltInEventHandlerId} from '../HIR/ObjectShape';
25 import {
26 eachInstructionOperand,
27 eachInstructionValueOperand,
@@ -183,6 +185,11 @@ function refTypeOfType(place: Place): RefAccessType {
185 }
186 }
187
188 +function isEventHandlerType(identifier: Identifier): boolean {
189 + const type = identifier.type;
190 + return type.kind === 'Function' && type.shapeId === BuiltInEventHandlerId;
191 +}
192 +
193 function tyEqual(a: RefAccessType, b: RefAccessType): boolean {
194 if (a.kind !== b.kind) {
195 return false;
@@ -519,6 +526,9 @@ function validateNoRefAccessInRenderImpl(
526 */
527 if (!didError) {
528 const isRefLValue = isUseRefType(instr.lvalue.identifier);
529 + const isEventHandlerLValue = isEventHandlerType(
530 + instr.lvalue.identifier,
531 + );
532 for (const operand of eachInstructionValueOperand(instr.value)) {
533 /**
534 * By default we check that function call operands are not refs,
@@ -526,29 +536,16 @@ function validateNoRefAccessInRenderImpl(
536 */
537 if (
538 isRefLValue ||
539 + isEventHandlerLValue ||
540 (hookKind != null &&
541 hookKind !== 'useState' &&
542 hookKind !== 'useReducer')
543 ) {
544 /**
534 - * Special cases:
535 - *
536 - * 1. the lvalue is a ref
537 - * In general passing a ref to a function may access that ref
538 - * value during render, so we disallow it.
539 - *
540 - * The main exception is the "mergeRefs" pattern, ie a function
541 - * that accepts multiple refs as arguments (or an array of refs)
542 - * and returns a new, aggregated ref. If the lvalue is a ref,
543 - * we assume that the user is doing this pattern and allow passing
544 - * refs.
545 - *
546 - * Eg `const mergedRef = mergeRefs(ref1, ref2)`
547 - *
548 - * 2. calling hooks
549 - *
550 - * Hooks are independently checked to ensure they don't access refs
551 - * during render.
545 + * Allow passing refs or ref-accessing functions when:
546 + * 1. lvalue is a ref (mergeRefs pattern: `mergeRefs(ref1, ref2)`)
547 + * 2. lvalue is an event handler (DOM events execute outside render)
548 + * 3. calling hooks (independently validated for ref safety)
549 */
550 validateNoDirectRefValueAccess(errors, operand, env);
551 } else if (interpolatedAsJsx.has(instr.lvalue.identifier.id)) {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-async-event-handler-wrapper.expect.md new
+148
@@ -0,0 +1,148 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableInferEventHandlers
6 +import {useRef} from 'react';
7 +
8 +// Simulates react-hook-form's handleSubmit
9 +function handleSubmit<T>(callback: (data: T) => void | Promise<void>) {
10 + return (event: any) => {
11 + event.preventDefault();
12 + callback({} as T);
13 + };
14 +}
15 +
16 +// Simulates an upload function
17 +async function upload(file: any): Promise<{blob: {url: string}}> {
18 + return {blob: {url: 'https://example.com/file.jpg'}};
19 +}
20 +
21 +interface SignatureRef {
22 + toFile(): any;
23 +}
24 +
25 +function Component() {
26 + const ref = useRef<SignatureRef>(null);
27 +
28 + const onSubmit = async (value: any) => {
29 + // This should be allowed: accessing ref.current in an async event handler
30 + // that's wrapped and passed to onSubmit prop
31 + let sigUrl: string;
32 + if (value.hasSignature) {
33 + const {blob} = await upload(ref.current?.toFile());
34 + sigUrl = blob?.url || '';
35 + } else {
36 + sigUrl = value.signature;
37 + }
38 + console.log('Signature URL:', sigUrl);
39 + };
40 +
41 + return (
42 + <form onSubmit={handleSubmit(onSubmit)}>
43 + <input type="text" name="signature" />
44 + <button type="submit">Submit</button>
45 + </form>
46 + );
47 +}
48 +
49 +export const FIXTURE_ENTRYPOINT = {
50 + fn: Component,
51 + params: [{}],
52 +};
53 +
54 +```
55 +
56 +## Code
57 +
58 +```javascript
59 +import { c as _c } from "react/compiler-runtime"; // @enableInferEventHandlers
60 +import { useRef } from "react";
61 +
62 +// Simulates react-hook-form's handleSubmit
63 +function handleSubmit(callback) {
64 + const $ = _c(2);
65 + let t0;
66 + if ($[0] !== callback) {
67 + t0 = (event) => {
68 + event.preventDefault();
69 + callback({} as T);
70 + };
71 + $[0] = callback;
72 + $[1] = t0;
73 + } else {
74 + t0 = $[1];
75 + }
76 + return t0;
77 +}
78 +
79 +// Simulates an upload function
80 +async function upload(file) {
81 + const $ = _c(1);
82 + let t0;
83 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
84 + t0 = { blob: { url: "https://example.com/file.jpg" } };
85 + $[0] = t0;
86 + } else {
87 + t0 = $[0];
88 + }
89 + return t0;
90 +}
91 +
92 +interface SignatureRef {
93 + toFile(): any;
94 +}
95 +
96 +function Component() {
97 + const $ = _c(4);
98 + const ref = useRef(null);
99 +
100 + const onSubmit = async (value) => {
101 + let sigUrl;
102 + if (value.hasSignature) {
103 + const { blob } = await upload(ref.current?.toFile());
104 + sigUrl = blob?.url || "";
105 + } else {
106 + sigUrl = value.signature;
107 + }
108 +
109 + console.log("Signature URL:", sigUrl);
110 + };
111 +
112 + const t0 = handleSubmit(onSubmit);
113 + let t1;
114 + let t2;
115 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
116 + t1 = <input type="text" name="signature" />;
117 + t2 = <button type="submit">Submit</button>;
118 + $[0] = t1;
119 + $[1] = t2;
120 + } else {
121 + t1 = $[0];
122 + t2 = $[1];
123 + }
124 + let t3;
125 + if ($[2] !== t0) {
126 + t3 = (
127 + <form onSubmit={t0}>
128 + {t1}
129 + {t2}
130 + </form>
131 + );
132 + $[2] = t0;
133 + $[3] = t3;
134 + } else {
135 + t3 = $[3];
136 + }
137 + return t3;
138 +}
139 +
140 +export const FIXTURE_ENTRYPOINT = {
141 + fn: Component,
142 + params: [{}],
143 +};
144 +
145 +```
146 +
147 +### Eval output
148 +(kind: ok) <form><input type="text" name="signature"><button type="submit">Submit</button></form>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-async-event-handler-wrapper.tsx new
+48
@@ -0,0 +1,48 @@
1 +// @enableInferEventHandlers
2 +import {useRef} from 'react';
3 +
4 +// Simulates react-hook-form's handleSubmit
5 +function handleSubmit<T>(callback: (data: T) => void | Promise<void>) {
6 + return (event: any) => {
7 + event.preventDefault();
8 + callback({} as T);
9 + };
10 +}
11 +
12 +// Simulates an upload function
13 +async function upload(file: any): Promise<{blob: {url: string}}> {
14 + return {blob: {url: 'https://example.com/file.jpg'}};
15 +}
16 +
17 +interface SignatureRef {
18 + toFile(): any;
19 +}
20 +
21 +function Component() {
22 + const ref = useRef<SignatureRef>(null);
23 +
24 + const onSubmit = async (value: any) => {
25 + // This should be allowed: accessing ref.current in an async event handler
26 + // that's wrapped and passed to onSubmit prop
27 + let sigUrl: string;
28 + if (value.hasSignature) {
29 + const {blob} = await upload(ref.current?.toFile());
30 + sigUrl = blob?.url || '';
31 + } else {
32 + sigUrl = value.signature;
33 + }
34 + console.log('Signature URL:', sigUrl);
35 + };
36 +
37 + return (
38 + <form onSubmit={handleSubmit(onSubmit)}>
39 + <input type="text" name="signature" />
40 + <button type="submit">Submit</button>
41 + </form>
42 + );
43 +}
44 +
45 +export const FIXTURE_ENTRYPOINT = {
46 + fn: Component,
47 + params: [{}],
48 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-event-handler-wrapper.expect.md new
+101
@@ -0,0 +1,101 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableInferEventHandlers
6 +import {useRef} from 'react';
7 +
8 +// Simulates react-hook-form's handleSubmit or similar event handler wrappers
9 +function handleSubmit<T>(callback: (data: T) => void) {
10 + return (event: any) => {
11 + event.preventDefault();
12 + callback({} as T);
13 + };
14 +}
15 +
16 +function Component() {
17 + const ref = useRef<HTMLInputElement>(null);
18 +
19 + const onSubmit = (data: any) => {
20 + // This should be allowed: accessing ref.current in an event handler
21 + // that's wrapped by handleSubmit and passed to onSubmit prop
22 + if (ref.current !== null) {
23 + console.log(ref.current.value);
24 + }
25 + };
26 +
27 + return (
28 + <>
29 + <input ref={ref} />
30 + <form onSubmit={handleSubmit(onSubmit)}>
31 + <button type="submit">Submit</button>
32 + </form>
33 + </>
34 + );
35 +}
36 +
37 +export const FIXTURE_ENTRYPOINT = {
38 + fn: Component,
39 + params: [{}],
40 +};
41 +
42 +```
43 +
44 +## Code
45 +
46 +```javascript
47 +import { c as _c } from "react/compiler-runtime"; // @enableInferEventHandlers
48 +import { useRef } from "react";
49 +
50 +// Simulates react-hook-form's handleSubmit or similar event handler wrappers
51 +function handleSubmit(callback) {
52 + const $ = _c(2);
53 + let t0;
54 + if ($[0] !== callback) {
55 + t0 = (event) => {
56 + event.preventDefault();
57 + callback({} as T);
58 + };
59 + $[0] = callback;
60 + $[1] = t0;
61 + } else {
62 + t0 = $[1];
63 + }
64 + return t0;
65 +}
66 +
67 +function Component() {
68 + const $ = _c(1);
69 + const ref = useRef(null);
70 + let t0;
71 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
72 + const onSubmit = (data) => {
73 + if (ref.current !== null) {
74 + console.log(ref.current.value);
75 + }
76 + };
77 +
78 + t0 = (
79 + <>
80 + <input ref={ref} />
81 + <form onSubmit={handleSubmit(onSubmit)}>
82 + <button type="submit">Submit</button>
83 + </form>
84 + </>
85 + );
86 + $[0] = t0;
87 + } else {
88 + t0 = $[0];
89 + }
90 + return t0;
91 +}
92 +
93 +export const FIXTURE_ENTRYPOINT = {
94 + fn: Component,
95 + params: [{}],
96 +};
97 +
98 +```
99 +
100 +### Eval output
101 +(kind: ok) <input><form><button type="submit">Submit</button></form>
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-event-handler-wrapper.tsx new
+36
@@ -0,0 +1,36 @@
1 +// @enableInferEventHandlers
2 +import {useRef} from 'react';
3 +
4 +// Simulates react-hook-form's handleSubmit or similar event handler wrappers
5 +function handleSubmit<T>(callback: (data: T) => void) {
6 + return (event: any) => {
7 + event.preventDefault();
8 + callback({} as T);
9 + };
10 +}
11 +
12 +function Component() {
13 + const ref = useRef<HTMLInputElement>(null);
14 +
15 + const onSubmit = (data: any) => {
16 + // This should be allowed: accessing ref.current in an event handler
17 + // that's wrapped by handleSubmit and passed to onSubmit prop
18 + if (ref.current !== null) {
19 + console.log(ref.current.value);
20 + }
21 + };
22 +
23 + return (
24 + <>
25 + <input ref={ref} />
26 + <form onSubmit={handleSubmit(onSubmit)}>
27 + <button type="submit">Submit</button>
28 + </form>
29 + </>
30 + );
31 +}
32 +
33 +export const FIXTURE_ENTRYPOINT = {
34 + fn: Component,
35 + params: [{}],
36 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-value-in-custom-component-event-handler-wrapper.expect.md new
+69
@@ -0,0 +1,69 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableInferEventHandlers
6 +import {useRef} from 'react';
7 +
8 +// Simulates a custom component wrapper
9 +function CustomForm({onSubmit, children}: any) {
10 + return <form onSubmit={onSubmit}>{children}</form>;
11 +}
12 +
13 +// Simulates react-hook-form's handleSubmit
14 +function handleSubmit<T>(callback: (data: T) => void) {
15 + return (event: any) => {
16 + event.preventDefault();
17 + callback({} as T);
18 + };
19 +}
20 +
21 +function Component() {
22 + const ref = useRef<HTMLInputElement>(null);
23 +
24 + const onSubmit = (data: any) => {
25 + // This should error: passing function with ref access to custom component
26 + // event handler, even though it would be safe on a native <form>
27 + if (ref.current !== null) {
28 + console.log(ref.current.value);
29 + }
30 + };
31 +
32 + return (
33 + <>
34 + <input ref={ref} />
35 + <CustomForm onSubmit={handleSubmit(onSubmit)}>
36 + <button type="submit">Submit</button>
37 + </CustomForm>
38 + </>
39 + );
40 +}
41 +
42 +export const FIXTURE_ENTRYPOINT = {
43 + fn: Component,
44 + params: [{}],
45 +};
46 +
47 +```
48 +
49 +
50 +## Error
51 +
52 +```
53 +Found 1 error:
54 +
55 +Error: Cannot access refs during render
56 +
57 +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).
58 +
59 +error.ref-value-in-custom-component-event-handler-wrapper.ts:31:41
60 + 29 | <>
61 + 30 | <input ref={ref} />
62 +> 31 | <CustomForm onSubmit={handleSubmit(onSubmit)}>
63 + | ^^^^^^^^ Passing a ref to a function may read its value during render
64 + 32 | <button type="submit">Submit</button>
65 + 33 | </CustomForm>
66 + 34 | </>
67 +```
68 +
69 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-value-in-custom-component-event-handler-wrapper.tsx new
+41
@@ -0,0 +1,41 @@
1 +// @enableInferEventHandlers
2 +import {useRef} from 'react';
3 +
4 +// Simulates a custom component wrapper
5 +function CustomForm({onSubmit, children}: any) {
6 + return <form onSubmit={onSubmit}>{children}</form>;
7 +}
8 +
9 +// Simulates react-hook-form's handleSubmit
10 +function handleSubmit<T>(callback: (data: T) => void) {
11 + return (event: any) => {
12 + event.preventDefault();
13 + callback({} as T);
14 + };
15 +}
16 +
17 +function Component() {
18 + const ref = useRef<HTMLInputElement>(null);
19 +
20 + const onSubmit = (data: any) => {
21 + // This should error: passing function with ref access to custom component
22 + // event handler, even though it would be safe on a native <form>
23 + if (ref.current !== null) {
24 + console.log(ref.current.value);
25 + }
26 + };
27 +
28 + return (
29 + <>
30 + <input ref={ref} />
31 + <CustomForm onSubmit={handleSubmit(onSubmit)}>
32 + <button type="submit">Submit</button>
33 + </CustomForm>
34 + </>
35 + );
36 +}
37 +
38 +export const FIXTURE_ENTRYPOINT = {
39 + fn: Component,
40 + params: [{}],
41 +};
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-value-in-event-handler-wrapper.expect.md new
+55
@@ -0,0 +1,55 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableInferEventHandlers
6 +import {useRef} from 'react';
7 +
8 +// Simulates a handler wrapper
9 +function handleClick(value: any) {
10 + return () => {
11 + console.log(value);
12 + };
13 +}
14 +
15 +function Component() {
16 + const ref = useRef(null);
17 +
18 + // This should still error: passing ref.current directly to a wrapper
19 + // The ref value is accessed during render, not in the event handler
20 + return (
21 + <>
22 + <input ref={ref} />
23 + <button onClick={handleClick(ref.current)}>Click</button>
24 + </>
25 + );
26 +}
27 +
28 +export const FIXTURE_ENTRYPOINT = {
29 + fn: Component,
30 + params: [{}],
31 +};
32 +
33 +```
34 +
35 +
36 +## Error
37 +
38 +```
39 +Found 1 error:
40 +
41 +Error: Cannot access refs during render
42 +
43 +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).
44 +
45 +error.ref-value-in-event-handler-wrapper.ts:19:35
46 + 17 | <>
47 + 18 | <input ref={ref} />
48 +> 19 | <button onClick={handleClick(ref.current)}>Click</button>
49 + | ^^^^^^^^^^^ Cannot access ref value during render
50 + 20 | </>
51 + 21 | );
52 + 22 | }
53 +```
54 +
55 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.ref-value-in-event-handler-wrapper.tsx new
+27
@@ -0,0 +1,27 @@
1 +// @enableInferEventHandlers
2 +import {useRef} from 'react';
3 +
4 +// Simulates a handler wrapper
5 +function handleClick(value: any) {
6 + return () => {
7 + console.log(value);
8 + };
9 +}
10 +
11 +function Component() {
12 + const ref = useRef(null);
13 +
14 + // This should still error: passing ref.current directly to a wrapper
15 + // The ref value is accessed during render, not in the event handler
16 + return (
17 + <>
18 + <input ref={ref} />
19 + <button onClick={handleClick(ref.current)}>Click</button>
20 + </>
21 + );
22 +}
23 +
24 +export const FIXTURE_ENTRYPOINT = {
25 + fn: Component,
26 + params: [{}],
27 +};