[compiler] Allow passing refs to render helpers (#34006)
We infer render helpers as functions whose result is immediately interpolated into jsx. This is a very conservative approximation, to help with common cases like `<Foo>{props.renderItem(ref)}</Foo>`. The idea is similar to hooks that it's ultimately on the developer to catch ref-in-render validations (and the runtime detects them too), so we can be a bit more relaxed since there are valid reasons to use this pattern. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34006). * #34027 * #34026 * #34025 * #34024 * #34005 * __->__ #34006 * #34004
Joseph Savona committed
Jul 29, 2025 at 10:06 UTC
3f40eb73a8de2823ea5cab39496faef33aa3f2b1
5 files changed
+161
-1
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts
+49
-1
@@ -262,6 +262,20 @@ function validateNoRefAccessInRenderImpl(
262
env.set(place.identifier.id, type);
263
}
264
265
+ const interpolatedAsJsx = new Set<IdentifierId>();
266
+ for (const block of fn.body.blocks.values()) {
267
+ for (const instr of block.instructions) {
268
+ const {value} = instr;
269
+ if (value.kind === 'JsxExpression' || value.kind === 'JsxFragment') {
270
+ if (value.children != null) {
271
+ for (const child of value.children) {
272
+ interpolatedAsJsx.add(child.identifier.id);
273
+ }
274
+ }
275
+ }
276
+ }
277
+ }
278
+
279
for (let i = 0; (i == 0 || env.hasChanged()) && i < 10; i++) {
280
env.resetChanged();
281
returnValues = [];
@@ -414,7 +428,41 @@ function validateNoRefAccessInRenderImpl(
428
if (!didError) {
429
const isRefLValue = isUseRefType(instr.lvalue.identifier);
430
for (const operand of eachInstructionValueOperand(instr.value)) {
417
- if (hookKind != null) {
431
+ /**
432
+ * By default we check that function call operands are not refs,
433
+ * ref values, or functions that can access refs.
434
+ */
435
+ if (
436
+ isRefLValue ||
437
+ interpolatedAsJsx.has(instr.lvalue.identifier.id) ||
438
+ hookKind != null
439
+ ) {
440
+ /**
441
+ * Special cases:
442
+ *
443
+ * 1) the lvalue is a ref
444
+ * In general passing a ref to a function may access that ref
445
+ * value during render, so we disallow it.
446
+ *
447
+ * The main exception is the "mergeRefs" pattern, ie a function
448
+ * that accepts multiple refs as arguments (or an array of refs)
449
+ * and returns a new, aggregated ref. If the lvalue is a ref,
450
+ * we assume that the user is doing this pattern and allow passing
451
+ * refs.
452
+ *
453
+ * Eg `const mergedRef = mergeRefs(ref1, ref2)`
454
+ *
455
+ * 2) the lvalue is passed as a jsx child
456
+ *
457
+ * For example `<Foo>{renderHelper(ref)}</Foo>`. Here we have more
458
+ * context and infer that the ref is being passed to a component-like
459
+ * render function which attempts to obey the rules.
460
+ *
461
+ * 3) hooks
462
+ *
463
+ * Hooks are independently checked to ensure they don't access refs
464
+ * during render.
465
+ */
466
validateNoDirectRefValueAccess(errors, operand, env);
467
} else if (!isRefLValue) {
468
/**
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper-props-object.expect.md
new
+45
@@ -0,0 +1,45 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
6
+
7
+import {useRef} from 'react';
8
+
9
+function Component(props) {
10
+ const ref = useRef(null);
11
+
12
+ return <Foo>{props.render({ref})}</Foo>;
13
+}
14
+
15
+```
16
+
17
+## Code
18
+
19
+```javascript
20
+import { c as _c } from "react/compiler-runtime"; // @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
21
+
22
+import { useRef } from "react";
23
+
24
+function Component(props) {
25
+ const $ = _c(3);
26
+ const ref = useRef(null);
27
+
28
+ const T0 = Foo;
29
+ const t0 = props.render({ ref });
30
+ let t1;
31
+ if ($[0] !== T0 || $[1] !== t0) {
32
+ t1 = <T0>{t0}</T0>;
33
+ $[0] = T0;
34
+ $[1] = t0;
35
+ $[2] = t1;
36
+ } else {
37
+ t1 = $[2];
38
+ }
39
+ return t1;
40
+}
41
+
42
+```
43
+
44
+### Eval output
45
+(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper-props-object.js
new
+9
@@ -0,0 +1,9 @@
1
+// @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
2
+
3
+import {useRef} from 'react';
4
+
5
+function Component(props) {
6
+ const ref = useRef(null);
7
+
8
+ return <Foo>{props.render({ref})}</Foo>;
9
+}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper.expect.md
new
+49
@@ -0,0 +1,49 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
6
+
7
+import {useRef} from 'react';
8
+
9
+function Component(props) {
10
+ const ref = useRef(null);
11
+
12
+ return <Foo>{props.render(ref)}</Foo>;
13
+}
14
+
15
+```
16
+
17
+## Code
18
+
19
+```javascript
20
+import { c as _c } from "react/compiler-runtime"; // @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
21
+
22
+import { useRef } from "react";
23
+
24
+function Component(props) {
25
+ const $ = _c(4);
26
+ const ref = useRef(null);
27
+ let t0;
28
+ if ($[0] !== props.render) {
29
+ t0 = props.render(ref);
30
+ $[0] = props.render;
31
+ $[1] = t0;
32
+ } else {
33
+ t0 = $[1];
34
+ }
35
+ let t1;
36
+ if ($[2] !== t0) {
37
+ t1 = <Foo>{t0}</Foo>;
38
+ $[2] = t0;
39
+ $[3] = t1;
40
+ } else {
41
+ t1 = $[3];
42
+ }
43
+ return t1;
44
+}
45
+
46
+```
47
+
48
+### Eval output
49
+(kind: exception) Fixture not implemented
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper.js
new
+9
@@ -0,0 +1,9 @@
1
+// @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender
2
+
3
+import {useRef} from 'react';
4
+
5
+function Component(props) {
6
+ const ref = useRef(null);
7
+
8
+ return <Foo>{props.render(ref)}</Foo>;
9
+}