Skip to content

Commit d5d3049

Browse files
committed
[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.
1 parent 18ac0af commit d5d3049

File tree

4 files changed

+85
-5
lines changed

4 files changed

+85
-5
lines changed

compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,18 @@ function* generateInstructionTypes(
451451

452452
case 'JsxExpression':
453453
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+
}
454466
yield equation(left, {kind: 'Object', shapeId: BuiltInJsxId});
455467
break;
456468
}

compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -407,15 +407,28 @@ function validateNoRefAccessInRenderImpl(
407407
);
408408
}
409409
}
410+
/*
411+
* If we already reported an error on this instruction, don't report
412+
* duplicate errors
413+
*/
410414
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);
415416
for (const operand of eachInstructionValueOperand(instr.value)) {
416417
if (hookKind != null) {
417418
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+
*/
419432
validateNoRefPassedToFunction(
420433
errors,
421434
env,
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

0 commit comments

Comments
 (0)