@samitouri / QOS-React-1 / commits / 3f40eb73a8

[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 +}