From 3f40eb73a8de2823ea5cab39496faef33aa3f2b1 Mon Sep 17 00:00:00 2001 From: Joseph Savona <6425824+josephsavona@users.noreply.github.com> Date: Tue, 29 Jul 2025 10:06:23 -0700 Subject: [PATCH] [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 `{props.renderItem(ref)}`. 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 --- .../Validation/ValidateNoRefAccessInRender.ts | 50 ++++++++++++++++++- ...ef-to-render-helper-props-object.expect.md | 45 +++++++++++++++++ ...ssing-ref-to-render-helper-props-object.js | 9 ++++ ...low-passing-ref-to-render-helper.expect.md | 49 ++++++++++++++++++ .../allow-passing-ref-to-render-helper.js | 9 ++++ 5 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper-props-object.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper-props-object.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts index b4fb0d171a..758ee459af 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts @@ -262,6 +262,20 @@ function validateNoRefAccessInRenderImpl( env.set(place.identifier.id, type); } + const interpolatedAsJsx = new Set(); + for (const block of fn.body.blocks.values()) { + for (const instr of block.instructions) { + const {value} = instr; + if (value.kind === 'JsxExpression' || value.kind === 'JsxFragment') { + if (value.children != null) { + for (const child of value.children) { + interpolatedAsJsx.add(child.identifier.id); + } + } + } + } + } + for (let i = 0; (i == 0 || env.hasChanged()) && i < 10; i++) { env.resetChanged(); returnValues = []; @@ -414,7 +428,41 @@ function validateNoRefAccessInRenderImpl( if (!didError) { const isRefLValue = isUseRefType(instr.lvalue.identifier); for (const operand of eachInstructionValueOperand(instr.value)) { - if (hookKind != null) { + /** + * By default we check that function call operands are not refs, + * ref values, or functions that can access refs. + */ + if ( + isRefLValue || + interpolatedAsJsx.has(instr.lvalue.identifier.id) || + hookKind != null + ) { + /** + * Special cases: + * + * 1) the lvalue is a ref + * 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)` + * + * 2) the lvalue is passed as a jsx child + * + * For example `{renderHelper(ref)}`. Here we have more + * context and infer that the ref is being passed to a component-like + * render function which attempts to obey the rules. + * + * 3) hooks + * + * Hooks are independently checked to ensure they don't access refs + * during render. + */ validateNoDirectRefValueAccess(errors, operand, env); } else if (!isRefLValue) { /** diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper-props-object.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper-props-object.expect.md new file mode 100644 index 0000000000..f23ab16c16 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper-props-object.expect.md @@ -0,0 +1,45 @@ + +## Input + +```javascript +// @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender + +import {useRef} from 'react'; + +function Component(props) { + const ref = useRef(null); + + return {props.render({ref})}; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender + +import { useRef } from "react"; + +function Component(props) { + const $ = _c(3); + const ref = useRef(null); + + const T0 = Foo; + const t0 = props.render({ ref }); + let t1; + if ($[0] !== T0 || $[1] !== t0) { + t1 = {t0}; + $[0] = T0; + $[1] = t0; + $[2] = t1; + } else { + t1 = $[2]; + } + return t1; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper-props-object.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper-props-object.js new file mode 100644 index 0000000000..ab9ffe2ed3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper-props-object.js @@ -0,0 +1,9 @@ +// @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender + +import {useRef} from 'react'; + +function Component(props) { + const ref = useRef(null); + + return {props.render({ref})}; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper.expect.md new file mode 100644 index 0000000000..a0ad22fcaf --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper.expect.md @@ -0,0 +1,49 @@ + +## Input + +```javascript +// @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender + +import {useRef} from 'react'; + +function Component(props) { + const ref = useRef(null); + + return {props.render(ref)}; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender + +import { useRef } from "react"; + +function Component(props) { + const $ = _c(4); + const ref = useRef(null); + let t0; + if ($[0] !== props.render) { + t0 = props.render(ref); + $[0] = props.render; + $[1] = t0; + } else { + t0 = $[1]; + } + let t1; + if ($[2] !== t0) { + t1 = {t0}; + $[2] = t0; + $[3] = t1; + } else { + t1 = $[3]; + } + return t1; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper.js new file mode 100644 index 0000000000..7c5a70188f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-passing-ref-to-render-helper.js @@ -0,0 +1,9 @@ +// @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender + +import {useRef} from 'react'; + +function Component(props) { + const ref = useRef(null); + + return {props.render(ref)}; +}