Repository: facebook/react
Pull request: [compiler] Allow mergeRefs pattern (and detect refs passed as ref prop)


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

Changed files:
filename: compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts
patch: @@ -451,6 +451,18 @@ function* generateInstructionTypes(
 
     case 'JsxExpression':
     case 'JsxFragment': {
+      if (env.config.enableTreatRefLikeIdentifiersAsRefs) {
+        if (value.kind === 'JsxExpression') {
+          for (const prop of value.props) {
+            if (prop.kind === 'JsxAttribute' && prop.name === 'ref') {
+              yield equation(prop.place.identifier.type, {
+                kind: 'Object',
+                shapeId: BuiltInUseRefId,
+              });
+            }
+          }
+        }
+      }
       yield equation(left, {kind: 'Object', shapeId: BuiltInJsxId});
       break;
     }

filename: compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts
patch: @@ -407,15 +407,28 @@ function validateNoRefAccessInRenderImpl(
                 );
               }
             }
+            /*
+             * If we already reported an error on this instruction, don't report
+             * duplicate errors
+             */
             if (!didError) {
-              /*
-               * If we already reported an error on this instruction, don't report
-               * duplicate errors
-               */
+              const isRefLValue = isUseRefType(instr.lvalue.identifier);
               for (const operand of eachInstructionValueOperand(instr.value)) {
                 if (hookKind != null) {
                   validateNoDirectRefValueAccess(errors, operand, env);
-                } else {
+                } else if (!isRefLValue) {
+                  /**
+                   * 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)`
+                   */
                   validateNoRefPassedToFunction(
                     errors,
                     env,

filename: compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-merge-refs-pattern.expect.md
patch: @@ -0,0 +1,44 @@
+
+## Input
+
+```javascript
+// @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
+
+import {useRef} from 'react';
+
+function Component() {
+  const ref = useRef(null);
+  const ref2 = useRef(null);
+  const mergedRef = mergeRefs([ref], ref2);
+
+  return <Stringify ref={mergedRef} />;
+}
+
+```
+
+## Code
+
+```javascript
+import { c as _c } from "react/compiler-runtime"; // @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
+
+import { useRef } from "react";
+
+function Component() {
+  const $ = _c(1);
+  const ref = useRef(null);
+  const ref2 = useRef(null);
+  const mergedRef = mergeRefs([ref], ref2);
+  let t0;
+  if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
+    t0 = <Stringify ref={mergedRef} />;
+    $[0] = t0;
+  } else {
+    t0 = $[0];
+  }
+  return t0;
+}
+
+```
+      
+### Eval output
+(kind: exception) Fixture not implemented
\ No newline at end of file

filename: compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-merge-refs-pattern.js
patch: @@ -0,0 +1,11 @@
+// @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
+
+import {useRef} from 'react';
+
+function Component() {
+  const ref = useRef(null);
+  const ref2 = useRef(null);
+  const mergedRef = mergeRefs([ref], ref2);
+
+  return <Stringify ref={mergedRef} />;
+}