@samitouri / QOS-React / commits / 1d7e942da7

[compiler] Allow mergeRefs pattern (and detect refs passed as ref prop) (#34004)

Two related changes: * ValidateNoRefAccessInRender now allows the mergeRefs pattern, ie a function that aggregates multiple refs into a new ref. This is the main case where we have seen false positive no-ref-in-render errors. * Behind `@enableTreatRefLikeIdentifiersAsRefs`, we infer values passed as the `ref` prop to some JSX as refs. The second change is potentially helpful for situations such as ```js function Component({ref: parentRef}) { const childRef = useRef(null); const mergedRef = mergeRefs(parentRef, childRef); useEffect(() => { // generally accesses childRef, not mergedRef }, []); return <Foo ref={mergedRef} />; } ``` Ie where you create a merged ref but don't access its `.current` property. Without inferring `ref` props as refs, we'd fail to allow this merge refs case. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34004). * #34027 * #34026 * #34025 * #34024 * #34005 * #34006 * __->__ #34004

Joseph Savona committed Jul 29, 2025 at 10:06 UTC 1d7e942da76c794c3cbf52a8c214d90a69814a8f
4 files changed +85 -5
compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts
+12
@@ -451,6 +451,18 @@ function* generateInstructionTypes(
451
452 case 'JsxExpression':
453 case 'JsxFragment': {
454 + if (env.config.enableTreatRefLikeIdentifiersAsRefs) {
455 + if (value.kind === 'JsxExpression') {
456 + for (const prop of value.props) {
457 + if (prop.kind === 'JsxAttribute' && prop.name === 'ref') {
458 + yield equation(prop.place.identifier.type, {
459 + kind: 'Object',
460 + shapeId: BuiltInUseRefId,
461 + });
462 + }
463 + }
464 + }
465 + }
466 yield equation(left, {kind: 'Object', shapeId: BuiltInJsxId});
467 break;
468 }
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts
+18 -5
@@ -407,15 +407,28 @@ function validateNoRefAccessInRenderImpl(
407 );
408 }
409 }
410 + /*
411 + * If we already reported an error on this instruction, don't report
412 + * duplicate errors
413 + */
414 if (!didError) {
411 - /*
412 - * If we already reported an error on this instruction, don't report
413 - * duplicate errors
414 - */
415 + const isRefLValue = isUseRefType(instr.lvalue.identifier);
416 for (const operand of eachInstructionValueOperand(instr.value)) {
417 if (hookKind != null) {
418 validateNoDirectRefValueAccess(errors, operand, env);
418 - } else {
419 + } else if (!isRefLValue) {
420 + /**
421 + * In general passing a ref to a function may access that ref
422 + * value during render, so we disallow it.
423 + *
424 + * The main exception is the "mergeRefs" pattern, ie a function
425 + * that accepts multiple refs as arguments (or an array of refs)
426 + * and returns a new, aggregated ref. If the lvalue is a ref,
427 + * we assume that the user is doing this pattern and allow passing
428 + * refs.
429 + *
430 + * Eg `const mergedRef = mergeRefs(ref1, ref2)`
431 + */
432 validateNoRefPassedToFunction(
433 errors,
434 env,
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-merge-refs-pattern.expect.md new
+44
@@ -0,0 +1,44 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
6 +
7 +import {useRef} from 'react';
8 +
9 +function Component() {
10 + const ref = useRef(null);
11 + const ref2 = useRef(null);
12 + const mergedRef = mergeRefs([ref], ref2);
13 +
14 + return <Stringify ref={mergedRef} />;
15 +}
16 +
17 +```
18 +
19 +## Code
20 +
21 +```javascript
22 +import { c as _c } from "react/compiler-runtime"; // @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
23 +
24 +import { useRef } from "react";
25 +
26 +function Component() {
27 + const $ = _c(1);
28 + const ref = useRef(null);
29 + const ref2 = useRef(null);
30 + const mergedRef = mergeRefs([ref], ref2);
31 + let t0;
32 + if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
33 + t0 = <Stringify ref={mergedRef} />;
34 + $[0] = t0;
35 + } else {
36 + t0 = $[0];
37 + }
38 + return t0;
39 +}
40 +
41 +```
42 +
43 +### Eval output
44 +(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-merge-refs-pattern.js new
+11
@@ -0,0 +1,11 @@
1 +// @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
2 +
3 +import {useRef} from 'react';
4 +
5 +function Component() {
6 + const ref = useRef(null);
7 + const ref2 = useRef(null);
8 + const mergedRef = mergeRefs([ref], ref2);
9 +
10 + return <Stringify ref={mergedRef} />;
11 +}