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)}; +}