diff --git a/.eslintrc.js b/.eslintrc.js index 4e023cd9d333b..2c9ad7a4c925d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -608,6 +608,7 @@ module.exports = { symbol: 'readonly', SyntheticEvent: 'readonly', SyntheticMouseEvent: 'readonly', + SyntheticPointerEvent: 'readonly', Thenable: 'readonly', TimeoutID: 'readonly', WheelEventHandler: 'readonly', diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts index ba7396e0d7d13..f94870fc03032 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -608,7 +608,7 @@ export const EnvironmentConfigSchema = z.object({ * * Here the variables `ref` and `myRef` will be typed as Refs. */ - enableTreatRefLikeIdentifiersAsRefs: z.boolean().default(false), + enableTreatRefLikeIdentifiersAsRefs: z.boolean().default(true), /* * If specified a value, the compiler lowers any calls to `useContext` to use diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts index baaf40d67e7bb..eaf728db95119 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/ObjectShape.ts @@ -1211,6 +1211,8 @@ addObject(BUILTIN_SHAPES, BuiltInRefValueId, [ ['*', {kind: 'Object', shapeId: BuiltInRefValueId}], ]); +addObject(BUILTIN_SHAPES, ReanimatedSharedValueId, []); + addFunction( BUILTIN_SHAPES, [], diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReactivePlaces.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReactivePlaces.ts index 88faccd8cf3b6..19e220b235694 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReactivePlaces.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReactivePlaces.ts @@ -21,7 +21,6 @@ import { isStableType, isStableTypeContainer, isUseOperator, - isUseRefType, } from '../HIR'; import {PostDominator} from '../HIR/Dominator'; import { @@ -70,13 +69,6 @@ class StableSidemap { isStable: false, }); } - } else if ( - this.env.config.enableTreatRefLikeIdentifiersAsRefs && - isUseRefType(lvalue.identifier) - ) { - this.map.set(lvalue.identifier.id, { - isStable: true, - }); } break; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts b/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts index 596244b3834c9..488d988b9715d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/TypeInference/InferTypes.ts @@ -451,6 +451,18 @@ function* generateInstructionTypes( case 'JsxExpression': case 'JsxFragment': { + if (env.config.enableTreatRefLikeIdentifiersAsRefs) { + if (value.kind === 'JsxExpression') { + for (const prop of value.props) { + if (prop.kind === 'JsxAttribute' && prop.name === 'ref') { + yield equation(prop.place.identifier.type, { + kind: 'Object', + shapeId: BuiltInUseRefId, + }); + } + } + } + } yield equation(left, {kind: 'Object', shapeId: BuiltInJsxId}); break; } @@ -466,7 +478,36 @@ function* generateInstructionTypes( yield equation(left, returnType); break; } - case 'PropertyStore': + case 'PropertyStore': { + /** + * Infer types based on assignments to known object properties + * This is important for refs, where assignment to `.current` + * can help us infer that an object itself is a ref + */ + yield equation( + /** + * Our property type declarations are best-effort and we haven't tested + * using them to drive inference of rvalues from lvalues. We want to emit + * a Property type in order to infer refs from `.current` accesses, but + * stay conservative by not otherwise inferring anything about rvalues. + * So we use a dummy type here. + * + * TODO: consider using the rvalue type here + */ + makeType(), + // unify() only handles properties in the second position + { + kind: 'Property', + objectType: value.object.identifier.type, + objectName: getName(names, value.object.identifier.id), + propertyName: { + kind: 'literal', + value: value.property, + }, + }, + ); + break; + } case 'DeclareLocal': case 'RegExpLiteral': case 'MetaProperty': 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 d00302559bb4e..e1c17625f4895 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts @@ -5,7 +5,11 @@ * LICENSE file in the root directory of this source tree. */ -import {CompilerError, ErrorSeverity} from '../CompilerError'; +import { + CompilerDiagnostic, + CompilerError, + ErrorSeverity, +} from '../CompilerError'; import { BlockId, HIRFunction, @@ -23,6 +27,7 @@ import { eachTerminalOperand, } from '../HIR/visitors'; import {Err, Ok, Result} from '../Utils/Result'; +import {retainWhere} from '../Utils/utils'; /** * Validates that a function does not access a ref value during render. This includes a partial check @@ -75,8 +80,18 @@ type RefAccessRefType = type RefFnType = {readRefEffect: boolean; returnType: RefAccessType}; -class Env extends Map { +class Env { #changed = false; + #data: Map = new Map(); + #temporaries: Map = new Map(); + + lookup(place: Place): Place { + return this.#temporaries.get(place.identifier.id) ?? place; + } + + define(place: Place, value: Place): void { + this.#temporaries.set(place.identifier.id, value); + } resetChanged(): void { this.#changed = false; @@ -86,8 +101,14 @@ class Env extends Map { return this.#changed; } - override set(key: IdentifierId, value: RefAccessType): this { - const cur = this.get(key); + get(key: IdentifierId): RefAccessType | undefined { + const operandId = this.#temporaries.get(key)?.identifier.id ?? key; + return this.#data.get(operandId); + } + + set(key: IdentifierId, value: RefAccessType): this { + const operandId = this.#temporaries.get(key)?.identifier.id ?? key; + const cur = this.#data.get(operandId); const widenedValue = joinRefAccessTypes(value, cur ?? {kind: 'None'}); if ( !(cur == null && widenedValue.kind === 'None') && @@ -95,7 +116,8 @@ class Env extends Map { ) { this.#changed = true; } - return super.set(key, widenedValue); + this.#data.set(operandId, widenedValue); + return this; } } @@ -103,9 +125,48 @@ export function validateNoRefAccessInRender( fn: HIRFunction, ): Result { const env = new Env(); + collectTemporariesSidemap(fn, env); return validateNoRefAccessInRenderImpl(fn, env).map(_ => undefined); } +function collectTemporariesSidemap(fn: HIRFunction, env: Env): void { + for (const block of fn.body.blocks.values()) { + for (const instr of block.instructions) { + const {lvalue, value} = instr; + switch (value.kind) { + case 'LoadLocal': { + const temp = env.lookup(value.place); + if (temp != null) { + env.define(lvalue, temp); + } + break; + } + case 'StoreLocal': { + const temp = env.lookup(value.value); + if (temp != null) { + env.define(lvalue, temp); + env.define(value.lvalue.place, temp); + } + break; + } + case 'PropertyLoad': { + if ( + isUseRefType(value.object.identifier) && + value.property === 'current' + ) { + continue; + } + const temp = env.lookup(value.object); + if (temp != null) { + env.define(lvalue, temp); + } + break; + } + } + } + } +} + function refTypeOfType(place: Place): RefAccessType { if (isRefValueType(place.identifier)) { return {kind: 'RefValue'}; @@ -258,12 +319,27 @@ 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 = []; - const safeBlocks = new Map(); + const safeBlocks: Array<{block: BlockId; ref: RefId}> = []; const errors = new CompilerError(); for (const [, block] of fn.body.blocks) { + retainWhere(safeBlocks, entry => entry.block !== block.id); for (const phi of block.phis) { env.set( phi.place.identifier.id, @@ -385,28 +461,79 @@ function validateNoRefAccessInRenderImpl( const hookKind = getHookKindForType(fn.env, callee.identifier.type); let returnType: RefAccessType = {kind: 'None'}; const fnType = env.get(callee.identifier.id); + let didError = false; if (fnType?.kind === 'Structure' && fnType.fn !== null) { returnType = fnType.fn.returnType; if (fnType.fn.readRefEffect) { - errors.push({ - severity: ErrorSeverity.InvalidReact, - reason: - 'This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef)', - loc: callee.loc, - description: - callee.identifier.name !== null && - callee.identifier.name.kind === 'named' - ? `Function \`${callee.identifier.name.value}\` accesses a ref` - : null, - suggestions: null, - }); + didError = true; + errors.pushDiagnostic( + CompilerDiagnostic.create({ + severity: ErrorSeverity.InvalidReact, + category: 'Cannot access refs during render', + description: ERROR_DESCRIPTION, + }).withDetail({ + kind: 'error', + loc: callee.loc, + message: `This function accesses a ref value`, + }), + ); } } - for (const operand of eachInstructionValueOperand(instr.value)) { - if (hookKind != null) { - validateNoDirectRefValueAccess(errors, operand, env); - } else { - validateNoRefAccess(errors, env, operand, operand.loc); + /* + * If we already reported an error on this instruction, don't report + * duplicate errors + */ + if (!didError) { + const isRefLValue = isUseRefType(instr.lvalue.identifier); + for (const operand of eachInstructionValueOperand(instr.value)) { + /** + * By default we check that function call operands are not refs, + * ref values, or functions that can access refs. + */ + if ( + isRefLValue || + (hookKind != null && + hookKind !== 'useState' && + hookKind !== 'useReducer') + ) { + /** + * 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. calling hooks + * + * Hooks are independently checked to ensure they don't access refs + * during render. + */ + validateNoDirectRefValueAccess(errors, operand, env); + } else if (interpolatedAsJsx.has(instr.lvalue.identifier.id)) { + /** + * Special case: 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. + */ + validateNoRefValueAccess(errors, env, operand); + } else { + validateNoRefPassedToFunction( + errors, + env, + operand, + operand.loc, + ); + } } } env.set(instr.lvalue.identifier.id, returnType); @@ -439,23 +566,39 @@ function validateNoRefAccessInRenderImpl( case 'PropertyStore': case 'ComputedDelete': case 'ComputedStore': { - const safe = safeBlocks.get(block.id); const target = env.get(instr.value.object.identifier.id); + let safe: (typeof safeBlocks)['0'] | null | undefined = null; if ( instr.value.kind === 'PropertyStore' && - safe != null && - target?.kind === 'Ref' && - target.refId === safe + target != null && + target.kind === 'Ref' ) { - safeBlocks.delete(block.id); + safe = safeBlocks.find(entry => entry.ref === target.refId); + } + if (safe != null) { + retainWhere(safeBlocks, entry => entry !== safe); } else { - validateNoRefAccess(errors, env, instr.value.object, instr.loc); + validateNoRefUpdate(errors, env, instr.value.object, instr.loc); } - for (const operand of eachInstructionValueOperand(instr.value)) { - if (operand === instr.value.object) { - continue; + if ( + instr.value.kind === 'ComputedDelete' || + instr.value.kind === 'ComputedStore' + ) { + validateNoRefValueAccess(errors, env, instr.value.property); + } + if ( + instr.value.kind === 'ComputedStore' || + instr.value.kind === 'PropertyStore' + ) { + validateNoDirectRefValueAccess(errors, instr.value.value, env); + const type = env.get(instr.value.value.identifier.id); + if (type != null && type.kind === 'Structure') { + let objectType: RefAccessType = type; + if (target != null) { + objectType = joinRefAccessTypes(objectType, target); + } + env.set(instr.value.object.identifier.id, objectType); } - validateNoRefValueAccess(errors, env, operand); } break; } @@ -535,8 +678,11 @@ function validateNoRefAccessInRenderImpl( if (block.terminal.kind === 'if') { const test = env.get(block.terminal.test.identifier.id); - if (test?.kind === 'Guard') { - safeBlocks.set(block.terminal.consequent, test.refId); + if ( + test?.kind === 'Guard' && + safeBlocks.find(entry => entry.ref === test.refId) == null + ) { + safeBlocks.push({block: block.terminal.fallthrough, ref: test.refId}); } } @@ -583,18 +729,17 @@ function destructure( function guardCheck(errors: CompilerError, operand: Place, env: Env): void { if (env.get(operand.identifier.id)?.kind === 'Guard') { - errors.push({ - severity: ErrorSeverity.InvalidReact, - reason: - 'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)', - loc: operand.loc, - description: - operand.identifier.name !== null && - operand.identifier.name.kind === 'named' - ? `Cannot access ref value \`${operand.identifier.name.value}\`` - : null, - suggestions: null, - }); + errors.pushDiagnostic( + CompilerDiagnostic.create({ + severity: ErrorSeverity.InvalidReact, + category: 'Cannot access refs during render', + description: ERROR_DESCRIPTION, + }).withDetail({ + kind: 'error', + loc: operand.loc, + message: `Cannot access ref value during render`, + }), + ); } } @@ -608,22 +753,21 @@ function validateNoRefValueAccess( type?.kind === 'RefValue' || (type?.kind === 'Structure' && type.fn?.readRefEffect) ) { - errors.push({ - severity: ErrorSeverity.InvalidReact, - reason: - 'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)', - loc: (type.kind === 'RefValue' && type.loc) || operand.loc, - description: - operand.identifier.name !== null && - operand.identifier.name.kind === 'named' - ? `Cannot access ref value \`${operand.identifier.name.value}\`` - : null, - suggestions: null, - }); + errors.pushDiagnostic( + CompilerDiagnostic.create({ + severity: ErrorSeverity.InvalidReact, + category: 'Cannot access refs during render', + description: ERROR_DESCRIPTION, + }).withDetail({ + kind: 'error', + loc: (type.kind === 'RefValue' && type.loc) || operand.loc, + message: `Cannot access ref value during render`, + }), + ); } } -function validateNoRefAccess( +function validateNoRefPassedToFunction( errors: CompilerError, env: Env, operand: Place, @@ -635,18 +779,39 @@ function validateNoRefAccess( type?.kind === 'RefValue' || (type?.kind === 'Structure' && type.fn?.readRefEffect) ) { - errors.push({ - severity: ErrorSeverity.InvalidReact, - reason: - 'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)', - loc: (type.kind === 'RefValue' && type.loc) || loc, - description: - operand.identifier.name !== null && - operand.identifier.name.kind === 'named' - ? `Cannot access ref value \`${operand.identifier.name.value}\`` - : null, - suggestions: null, - }); + errors.pushDiagnostic( + CompilerDiagnostic.create({ + severity: ErrorSeverity.InvalidReact, + category: 'Cannot access refs during render', + description: ERROR_DESCRIPTION, + }).withDetail({ + kind: 'error', + loc: (type.kind === 'RefValue' && type.loc) || loc, + message: `Passing a ref to a function may read its value during render`, + }), + ); + } +} + +function validateNoRefUpdate( + errors: CompilerError, + env: Env, + operand: Place, + loc: SourceLocation, +): void { + const type = destructure(env.get(operand.identifier.id)); + if (type?.kind === 'Ref' || type?.kind === 'RefValue') { + errors.pushDiagnostic( + CompilerDiagnostic.create({ + severity: ErrorSeverity.InvalidReact, + category: 'Cannot access refs during render', + description: ERROR_DESCRIPTION, + }).withDetail({ + kind: 'error', + loc: (type.kind === 'RefValue' && type.loc) || loc, + message: `Cannot update ref during render`, + }), + ); } } @@ -657,17 +822,22 @@ function validateNoDirectRefValueAccess( ): void { const type = destructure(env.get(operand.identifier.id)); if (type?.kind === 'RefValue') { - errors.push({ - severity: ErrorSeverity.InvalidReact, - reason: - 'Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef)', - loc: type.loc ?? operand.loc, - description: - operand.identifier.name !== null && - operand.identifier.name.kind === 'named' - ? `Cannot access ref value \`${operand.identifier.name.value}\`` - : null, - suggestions: null, - }); + errors.pushDiagnostic( + CompilerDiagnostic.create({ + severity: ErrorSeverity.InvalidReact, + category: 'Cannot access refs during render', + description: ERROR_DESCRIPTION, + }).withDetail({ + kind: 'error', + loc: type.loc ?? operand.loc, + message: `Cannot access ref value during render`, + }), + ); } } + +const ERROR_DESCRIPTION = + 'React refs are values that are not needed for rendering. Refs should only be accessed ' + + 'outside of render, such as in event handlers or effects. ' + + 'Accessing a ref value (the `current` property) during render can cause your component ' + + 'not to update as expected (https://react.dev/reference/react/useRef)'; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-ref-accessing-function-to-object-property-if-not-mutated.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-ref-accessing-function-to-object-property-if-not-mutated.expect.md new file mode 100644 index 0000000000000..b5fc0a9dc7563 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-ref-accessing-function-to-object-property-if-not-mutated.expect.md @@ -0,0 +1,52 @@ + +## Input + +```javascript +import {useRef} from 'react'; +import {Stringify} from 'shared-runtime'; + +function Component(props) { + const ref = useRef(props.value); + const object = {}; + object.foo = () => ref.current; + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useRef } from "react"; +import { Stringify } from "shared-runtime"; + +function Component(props) { + const $ = _c(1); + const ref = useRef(props.value); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + const object = {}; + object.foo = () => ref.current; + t0 = ; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ value: 42 }], +}; + +``` + +### Eval output +(kind: ok)
{"object":{"foo":{"kind":"Function","result":42}},"shouldInvokeFns":true}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-ref-accessing-function-to-object-property-if-not-mutated.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-ref-accessing-function-to-object-property-if-not-mutated.js new file mode 100644 index 0000000000000..2c84772dca06f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-assigning-ref-accessing-function-to-object-property-if-not-mutated.js @@ -0,0 +1,14 @@ +import {useRef} from 'react'; +import {Stringify} from 'shared-runtime'; + +function Component(props) { + const ref = useRef(props.value); + const object = {}; + object.foo = () => ref.current; + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-merge-refs-pattern.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-merge-refs-pattern.expect.md new file mode 100644 index 0000000000000..6933edef4678e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-merge-refs-pattern.expect.md @@ -0,0 +1,44 @@ + +## Input + +```javascript +// @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender + +import {useRef} from 'react'; + +function Component() { + const ref = useRef(null); + const ref2 = useRef(null); + const mergedRef = mergeRefs([ref], ref2); + + return ; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender + +import { useRef } from "react"; + +function Component() { + const $ = _c(1); + const ref = useRef(null); + const ref2 = useRef(null); + const mergedRef = mergeRefs([ref], ref2); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = ; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} + +``` + +### 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-merge-refs-pattern.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-merge-refs-pattern.js new file mode 100644 index 0000000000000..91c5f0828490d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-merge-refs-pattern.js @@ -0,0 +1,11 @@ +// @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender + +import {useRef} from 'react'; + +function Component() { + const ref = useRef(null); + const ref2 = useRef(null); + const mergedRef = mergeRefs([ref], ref2); + + return ; +} 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 0000000000000..f23ab16c16bdd --- /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 0000000000000..ab9ffe2ed37f1 --- /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 0000000000000..a0ad22fcaf417 --- /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 0000000000000..7c5a70188fab0 --- /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)}; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect-indirect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect-indirect.expect.md index 7c1f5ad3727b8..6cf97f6c35157 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect-indirect.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect-indirect.expect.md @@ -27,6 +27,7 @@ function Component() { } function Child({ref}) { + 'use no memo'; // This violates the rules of React, so we access the ref in a child // component return ref.current; @@ -100,8 +101,10 @@ function Component() { return t6; } -function Child(t0) { - const { ref } = t0; +function Child({ ref }) { + "use no memo"; + // This violates the rules of React, so we access the ref in a child + // component return ref.current; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect-indirect.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect-indirect.js index 69429049022ab..4320b5871daf5 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect-indirect.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect-indirect.js @@ -23,6 +23,7 @@ function Component() { } function Child({ref}) { + 'use no memo'; // This violates the rules of React, so we access the ref in a child // component return ref.current; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect.expect.md index 3bdf358611db6..009d504da25b3 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect.expect.md @@ -23,6 +23,7 @@ function Component() { } function Child({ref}) { + 'use no memo'; // This violates the rules of React, so we access the ref in a child // component return ref.current; @@ -86,8 +87,10 @@ function Component() { return t5; } -function Child(t0) { - const { ref } = t0; +function Child({ ref }) { + "use no memo"; + // This violates the rules of React, so we access the ref in a child + // component return ref.current; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect.js index efba0547ebcd8..54a1dc22c3a6f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-effect.js @@ -19,6 +19,7 @@ function Component() { } function Child({ref}) { + 'use no memo'; // This violates the rules of React, so we access the ref in a child // component return ref.current; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-unused-callback-nested.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-unused-callback-nested.expect.md index 3584faf699f86..26e996017ebd7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-unused-callback-nested.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-unused-callback-nested.expect.md @@ -25,6 +25,7 @@ function Component() { } function Child({ref}) { + 'use no memo'; // This violates the rules of React, so we access the ref in a child // component return ref.current; @@ -83,8 +84,10 @@ function Component() { } function _temp() {} -function Child(t0) { - const { ref } = t0; +function Child({ ref }) { + "use no memo"; + // This violates the rules of React, so we access the ref in a child + // component return ref.current; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-unused-callback-nested.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-unused-callback-nested.js index 1e402886376c3..dcd8540e2a4b4 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-unused-callback-nested.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-access-in-unused-callback-nested.js @@ -21,6 +21,7 @@ function Component() { } function Child({ref}) { + 'use no memo'; // This violates the rules of React, so we access the ref in a child // component return ref.current; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-lazy-initialization-with-logical.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-lazy-initialization-with-logical.expect.md new file mode 100644 index 0000000000000..3540e842f67b8 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-lazy-initialization-with-logical.expect.md @@ -0,0 +1,68 @@ + +## Input + +```javascript +// @validateRefAccessDuringRender + +import {useRef} from 'react'; + +function Component(props) { + const ref = useRef(null); + if (ref.current == null) { + // the logical means the ref write is in a different block + // from the if consequent. this tests that the "safe" blocks + // extend up to the if's fallthrough + ref.current = props.unknownKey ?? props.value; + } + return ; +} + +function Child({ref}) { + 'use no memo'; + return ref.current; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validateRefAccessDuringRender + +import { useRef } from "react"; + +function Component(props) { + const $ = _c(1); + const ref = useRef(null); + if (ref.current == null) { + ref.current = props.unknownKey ?? props.value; + } + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = ; + $[0] = t0; + } else { + t0 = $[0]; + } + return t0; +} + +function Child({ ref }) { + "use no memo"; + return ref.current; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ value: 42 }], +}; + +``` + +### Eval output +(kind: ok) 42 \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-lazy-initialization-with-logical.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-lazy-initialization-with-logical.js new file mode 100644 index 0000000000000..2e1b03a28dadf --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-lazy-initialization-with-logical.js @@ -0,0 +1,24 @@ +// @validateRefAccessDuringRender + +import {useRef} from 'react'; + +function Component(props) { + const ref = useRef(null); + if (ref.current == null) { + // the logical means the ref write is in a different block + // from the if consequent. this tests that the "safe" blocks + // extend up to the if's fallthrough + ref.current = props.unknownKey ?? props.value; + } + return ; +} + +function Child({ref}) { + 'use no memo'; + return ref.current; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capture-ref-for-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capture-ref-for-mutation.expect.md index 36aba1765a445..cb2256a187fac 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capture-ref-for-mutation.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.capture-ref-for-mutation.expect.md @@ -32,48 +32,30 @@ export const FIXTURE_ENTRYPOINT = { ## Error ``` -Found 4 errors: +Found 2 errors: -Error: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef) +Error: Cannot access refs during render -error.capture-ref-for-mutation.ts:12:13 - 10 | }; - 11 | const moveLeft = { -> 12 | handler: handleKey('left')(), - | ^^^^^^^^^^^^^^^^^ This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef) - 13 | }; - 14 | const moveRight = { - 15 | handler: handleKey('right')(), - -Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) +React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef) error.capture-ref-for-mutation.ts:12:13 10 | }; 11 | const moveLeft = { > 12 | handler: handleKey('left')(), - | ^^^^^^^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) + | ^^^^^^^^^^^^^^^^^ This function accesses a ref value 13 | }; 14 | const moveRight = { 15 | handler: handleKey('right')(), -Error: This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef) - -error.capture-ref-for-mutation.ts:15:13 - 13 | }; - 14 | const moveRight = { -> 15 | handler: handleKey('right')(), - | ^^^^^^^^^^^^^^^^^^ This function accesses a ref value (the `current` property), which may not be accessed during render. (https://react.dev/reference/react/useRef) - 16 | }; - 17 | return [moveLeft, moveRight]; - 18 | } +Error: Cannot access refs during render -Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) +React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef) error.capture-ref-for-mutation.ts:15:13 13 | }; 14 | const moveRight = { > 15 | handler: handleKey('right')(), - | ^^^^^^^^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) + | ^^^^^^^^^^^^^^^^^^ This function accesses a ref value 16 | }; 17 | return [moveLeft, moveRight]; 18 | } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-ref-value.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-ref-value.expect.md index 63c70cb9f90c9..36949c65042d9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-ref-value.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hook-ref-value.expect.md @@ -22,24 +22,28 @@ export const FIXTURE_ENTRYPOINT = { ``` Found 2 errors: -Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) +Error: Cannot access refs during render + +React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef) error.hook-ref-value.ts:5:23 3 | function Component(props) { 4 | const ref = useRef(); > 5 | useEffect(() => {}, [ref.current]); - | ^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) + | ^^^^^^^^^^^ Cannot access ref value during render 6 | } 7 | 8 | export const FIXTURE_ENTRYPOINT = { -Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) +Error: Cannot access refs during render + +React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef) error.hook-ref-value.ts:5:23 3 | function Component(props) { 4 | const ref = useRef(); > 5 | useEffect(() => {}, [ref.current]); - | ^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) + | ^^^^^^^^^^^ Cannot access ref value during render 6 | } 7 | 8 | export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-during-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-during-render.expect.md index 123428f602497..989e68efd8809 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-during-render.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-during-render.expect.md @@ -17,13 +17,15 @@ function Component(props) { ``` Found 1 error: -Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) +Error: Cannot access refs during render + +React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef) error.invalid-access-ref-during-render.ts:4:16 2 | function Component(props) { 3 | const ref = useRef(null); > 4 | const value = ref.current; - | ^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) + | ^^^^^^^^^^^ Cannot access ref value during render 5 | return value; 6 | } 7 | diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer-init.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer-init.expect.md new file mode 100644 index 0000000000000..29fe24a22025e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer-init.expect.md @@ -0,0 +1,45 @@ + +## Input + +```javascript +import {useReducer, useRef} from 'react'; + +function Component(props) { + const ref = useRef(props.value); + const [state] = useReducer( + (state, action) => state + action, + 0, + init => ref.current + ); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; + +``` + + +## Error + +``` +Found 1 error: + +Error: Cannot access refs during render + +React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef) + +error.invalid-access-ref-in-reducer-init.ts:8:4 + 6 | (state, action) => state + action, + 7 | 0, +> 8 | init => ref.current + | ^^^^^^^^^^^^^^^^^^^ Passing a ref to a function may read its value during render + 9 | ); + 10 | + 11 | return ; +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer-init.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer-init.js new file mode 100644 index 0000000000000..df10b8a9ebd0f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer-init.js @@ -0,0 +1,17 @@ +import {useReducer, useRef} from 'react'; + +function Component(props) { + const ref = useRef(props.value); + const [state] = useReducer( + (state, action) => state + action, + 0, + init => ref.current + ); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer.expect.md new file mode 100644 index 0000000000000..f23560b4f69c7 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer.expect.md @@ -0,0 +1,41 @@ + +## Input + +```javascript +import {useReducer, useRef} from 'react'; + +function Component(props) { + const ref = useRef(props.value); + const [state] = useReducer(() => ref.current, null); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; + +``` + + +## Error + +``` +Found 1 error: + +Error: Cannot access refs during render + +React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef) + +error.invalid-access-ref-in-reducer.ts:5:29 + 3 | function Component(props) { + 4 | const ref = useRef(props.value); +> 5 | const [state] = useReducer(() => ref.current, null); + | ^^^^^^^^^^^^^^^^^ Passing a ref to a function may read its value during render + 6 | + 7 | return ; + 8 | } +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer.js new file mode 100644 index 0000000000000..135a78e0baef5 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-reducer.js @@ -0,0 +1,13 @@ +import {useReducer, useRef} from 'react'; + +function Component(props) { + const ref = useRef(props.value); + const [state] = useReducer(() => ref.current, null); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-render-mutate-object-with-ref-function.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-render-mutate-object-with-ref-function.expect.md new file mode 100644 index 0000000000000..a70fcf39b3d80 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-render-mutate-object-with-ref-function.expect.md @@ -0,0 +1,37 @@ + +## Input + +```javascript +import {useRef} from 'react'; + +function Component() { + const ref = useRef(null); + const object = {}; + object.foo = () => ref.current; + const refValue = object.foo(); + return
{refValue}
; +} + +``` + + +## Error + +``` +Found 1 error: + +Error: Cannot access refs during render + +React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef) + +error.invalid-access-ref-in-render-mutate-object-with-ref-function.ts:7:19 + 5 | const object = {}; + 6 | object.foo = () => ref.current; +> 7 | const refValue = object.foo(); + | ^^^^^^^^^^ This function accesses a ref value + 8 | return
{refValue}
; + 9 | } + 10 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-render-mutate-object-with-ref-function.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-render-mutate-object-with-ref-function.js new file mode 100644 index 0000000000000..9d3faac764b5a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-render-mutate-object-with-ref-function.js @@ -0,0 +1,9 @@ +import {useRef} from 'react'; + +function Component() { + const ref = useRef(null); + const object = {}; + object.foo = () => ref.current; + const refValue = object.foo(); + return
{refValue}
; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-state-initializer.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-state-initializer.expect.md new file mode 100644 index 0000000000000..dd6a64d9db748 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-state-initializer.expect.md @@ -0,0 +1,41 @@ + +## Input + +```javascript +import {useRef, useState} from 'react'; + +function Component(props) { + const ref = useRef(props.value); + const [state] = useState(() => ref.current); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; + +``` + + +## Error + +``` +Found 1 error: + +Error: Cannot access refs during render + +React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef) + +error.invalid-access-ref-in-state-initializer.ts:5:27 + 3 | function Component(props) { + 4 | const ref = useRef(props.value); +> 5 | const [state] = useState(() => ref.current); + | ^^^^^^^^^^^^^^^^^ Passing a ref to a function may read its value during render + 6 | + 7 | return ; + 8 | } +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-state-initializer.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-state-initializer.js new file mode 100644 index 0000000000000..c3f233023ec85 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-access-ref-in-state-initializer.js @@ -0,0 +1,13 @@ +import {useRef, useState} from 'react'; + +function Component(props) { + const ref = useRef(props.value); + const [state] = useState(() => ref.current); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{value: 42}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-aliased-ref-in-callback-invoked-during-render-.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-aliased-ref-in-callback-invoked-during-render-.expect.md index 1da271e5618e6..3aa5237533962 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-aliased-ref-in-callback-invoked-during-render-.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-aliased-ref-in-callback-invoked-during-render-.expect.md @@ -21,13 +21,15 @@ function Component(props) { ``` Found 1 error: -Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) +Error: Cannot access refs during render + +React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef) error.invalid-aliased-ref-in-callback-invoked-during-render-.ts:9:33 7 | return ; 8 | }; > 9 | return {props.items.map(item => renderItem(item))}; - | ^^^^^^^^^^^^^^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) + | ^^^^^^^^^^^^^^^^^^^^^^^^ Cannot access ref value during render 10 | } 11 | ``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-assign-current-inferred-ref-during-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-assign-current-inferred-ref-during-render.expect.md new file mode 100644 index 0000000000000..4f4ed63550d08 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-assign-current-inferred-ref-during-render.expect.md @@ -0,0 +1,36 @@ + +## Input + +```javascript +// @flow @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender +import {makeObject_Primitives} from 'shared-runtime'; + +component Example() { + const fooRef = makeObject_Primitives(); + fooRef.current = true; + + return ; +} + +``` + + +## Error + +``` +Found 1 error: + +Error: Cannot access refs during render + +React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef) + + 4 | component Example() { + 5 | const fooRef = makeObject_Primitives(); +> 6 | fooRef.current = true; + | ^^^^^^^^^^^^^^ Cannot update ref during render + 7 | + 8 | return ; + 9 | } +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-assign-current-inferred-ref-during-render.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-assign-current-inferred-ref-during-render.js new file mode 100644 index 0000000000000..39df293ba6258 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-assign-current-inferred-ref-during-render.js @@ -0,0 +1,9 @@ +// @flow @enableTreatRefLikeIdentifiersAsRefs @validateRefAccessDuringRender +import {makeObject_Primitives} from 'shared-runtime'; + +component Example() { + const fooRef = makeObject_Primitives(); + fooRef.current = true; + + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-ref-in-render.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-ref-in-render.expect.md index 556d9a26371b3..9f19d10b9d458 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-ref-in-render.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-disallow-mutating-ref-in-render.expect.md @@ -18,13 +18,15 @@ function Component() { ``` Found 1 error: -Error: Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) +Error: Cannot access refs during render + +React refs are values that are not needed for rendering. Refs should only be accessed outside of render, such as in event handlers or effects. Accessing a ref value (the `current` property) during render can cause your component not to update as expected (https://react.dev/reference/react/useRef) error.invalid-disallow-mutating-ref-in-render.ts:4:2 2 | function Component() { 3 | const ref = useRef(null); > 4 | ref.current = false; - | ^^^^^^^^^^^ Ref values (the `current` property) may not be accessed during render. (https://react.dev/reference/react/useRef) + | ^^^^^^^^^^^ Cannot update ref during render 5 | 6 | return