diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts index e0b0536f28cc6..1085d4c69e061 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts @@ -276,7 +276,7 @@ function runWithEnvironment( } if (env.config.validateNoSetStateInEffects) { - env.logErrors(validateNoSetStateInEffects(hir)); + env.logErrors(validateNoSetStateInEffects(hir, env)); } if (env.config.validateNoJSXInTryStatements) { 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 8e6816a3d5128..0ef41866a51c2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -660,6 +660,13 @@ export const EnvironmentConfigSchema = z.object({ * while its parent function remains uncompiled. */ validateNoDynamicallyCreatedComponentsOrHooks: z.boolean().default(false), + + /** + * When enabled, allows setState calls in effects when the value being set is + * derived from a ref. This is useful for patterns where initial layout measurements + * from refs need to be stored in state during mount. + */ + enableAllowSetStateFromRefsInEffects: z.boolean().default(true), }); export type EnvironmentConfig = z.infer; 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 eb053ac4196a2..abbb7d8476981 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoRefAccessInRender.ts @@ -639,12 +639,55 @@ function validateNoRefAccessInRenderImpl( case 'StartMemoize': case 'FinishMemoize': break; + case 'LoadGlobal': { + if (instr.value.binding.name === 'undefined') { + env.set(instr.lvalue.identifier.id, {kind: 'Nullable'}); + } + break; + } case 'Primitive': { if (instr.value.value == null) { env.set(instr.lvalue.identifier.id, {kind: 'Nullable'}); } break; } + case 'UnaryExpression': { + if (instr.value.operator === '!') { + const value = env.get(instr.value.value.identifier.id); + const refId = + value?.kind === 'RefValue' && value.refId != null + ? value.refId + : null; + if (refId !== null) { + /* + * Record an error suggesting the `if (ref.current == null)` pattern, + * but also record the lvalue as a guard so that we don't emit a second + * error for the write to the ref + */ + env.set(instr.lvalue.identifier.id, {kind: 'Guard', refId}); + errors.pushDiagnostic( + CompilerDiagnostic.create({ + category: ErrorCategory.Refs, + reason: 'Cannot access refs during render', + description: ERROR_DESCRIPTION, + }) + .withDetails({ + kind: 'error', + loc: instr.value.value.loc, + message: `Cannot access ref value during render`, + }) + .withDetails({ + kind: 'hint', + message: + 'To initialize a ref only once, check that the ref is null with the pattern `if (ref.current == null) { ref.current = ... }`', + }), + ); + break; + } + } + validateNoRefValueAccess(errors, env, instr.value.value); + break; + } case 'BinaryExpression': { const left = env.get(instr.value.left.identifier.id); const right = env.get(instr.value.right.identifier.id); diff --git a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInEffects.ts b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInEffects.ts index 19c2e7bc5b9ab..656c9a671487b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInEffects.ts @@ -11,16 +11,23 @@ import { ErrorCategory, } from '../CompilerError'; import { + Environment, HIRFunction, IdentifierId, isSetStateType, isUseEffectHookType, isUseInsertionEffectHookType, isUseLayoutEffectHookType, + isUseRefType, + isRefValueType, Place, } from '../HIR'; -import {eachInstructionValueOperand} from '../HIR/visitors'; +import { + eachInstructionLValue, + eachInstructionValueOperand, +} from '../HIR/visitors'; import {Result} from '../Utils/Result'; +import {Iterable_some} from '../Utils/utils'; /** * Validates against calling setState in the body of an effect (useEffect and friends), @@ -32,6 +39,7 @@ import {Result} from '../Utils/Result'; */ export function validateNoSetStateInEffects( fn: HIRFunction, + env: Environment, ): Result { const setStateFunctions: Map = new Map(); const errors = new CompilerError(); @@ -72,6 +80,7 @@ export function validateNoSetStateInEffects( const callee = getSetStateCall( instr.value.loweredFunc.func, setStateFunctions, + env, ); if (callee !== null) { setStateFunctions.set(instr.lvalue.identifier.id, callee); @@ -129,9 +138,42 @@ export function validateNoSetStateInEffects( function getSetStateCall( fn: HIRFunction, setStateFunctions: Map, + env: Environment, ): Place | null { + const refDerivedValues: Set = new Set(); + + const isDerivedFromRef = (place: Place): boolean => { + return ( + refDerivedValues.has(place.identifier.id) || + isUseRefType(place.identifier) || + isRefValueType(place.identifier) + ); + }; + for (const [, block] of fn.body.blocks) { for (const instr of block.instructions) { + if (env.config.enableAllowSetStateFromRefsInEffects) { + const hasRefOperand = Iterable_some( + eachInstructionValueOperand(instr.value), + isDerivedFromRef, + ); + + if (hasRefOperand) { + for (const lvalue of eachInstructionLValue(instr)) { + refDerivedValues.add(lvalue.identifier.id); + } + } + + if ( + instr.value.kind === 'PropertyLoad' && + instr.value.property === 'current' && + (isUseRefType(instr.value.object.identifier) || + isRefValueType(instr.value.object.identifier)) + ) { + refDerivedValues.add(instr.lvalue.identifier.id); + } + } + switch (instr.value.kind) { case 'LoadLocal': { if (setStateFunctions.has(instr.value.place.identifier.id)) { @@ -161,6 +203,21 @@ function getSetStateCall( isSetStateType(callee.identifier) || setStateFunctions.has(callee.identifier.id) ) { + if (env.config.enableAllowSetStateFromRefsInEffects) { + const arg = instr.value.args.at(0); + if ( + arg !== undefined && + arg.kind === 'Identifier' && + refDerivedValues.has(arg.identifier.id) + ) { + /** + * The one special case where we allow setStates in effects is in the very specific + * scenario where the value being set is derived from a ref. For example this may + * be needed when initial layout measurements from refs need to be stored in state. + */ + return null; + } + } /* * TODO: once we support multiple locations per error, we should link to the * original Place in the case that setStateFunction.has(callee) diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-initialization-undefined.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-initialization-undefined.expect.md new file mode 100644 index 0000000000000..c355819320af0 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-initialization-undefined.expect.md @@ -0,0 +1,42 @@ + +## Input + +```javascript +//@flow +import {useRef} from 'react'; + +component C() { + const r = useRef(null); + if (r.current == undefined) { + r.current = 1; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: C, + params: [{}], +}; + +``` + +## Code + +```javascript +import { useRef } from "react"; + +function C() { + const r = useRef(null); + if (r.current == undefined) { + r.current = 1; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: C, + params: [{}], +}; + +``` + +### Eval output +(kind: ok) \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-initialization-undefined.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-initialization-undefined.js new file mode 100644 index 0000000000000..886f025287bbf --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-ref-initialization-undefined.js @@ -0,0 +1,14 @@ +//@flow +import {useRef} from 'react'; + +component C() { + const r = useRef(null); + if (r.current == undefined) { + r.current = 1; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: C, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-access-render-unary.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-access-render-unary.expect.md new file mode 100644 index 0000000000000..ce1be800a13bb --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-access-render-unary.expect.md @@ -0,0 +1,78 @@ + +## Input + +```javascript +//@flow +import {useRef} from 'react'; + +component C() { + const r = useRef(null); + const current = !r.current; + return
{current}
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: C, + params: [{}], +}; + +``` + + +## Error + +``` +Found 4 errors: + +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 C() { + 5 | const r = useRef(null); +> 6 | const current = !r.current; + | ^^^^^^^^^ Cannot access ref value during render + 7 | return
{current}
; + 8 | } + 9 | + +To initialize a ref only once, check that the ref is null with the pattern `if (ref.current == null) { ref.current = ... }` + +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 C() { + 5 | const r = useRef(null); +> 6 | const current = !r.current; + | ^^^^^^^^^^ Cannot access ref value during render + 7 | return
{current}
; + 8 | } + 9 | + +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). + + 5 | const r = useRef(null); + 6 | const current = !r.current; +> 7 | return
{current}
; + | ^^^^^^^ Cannot access ref value during render + 8 | } + 9 | + 10 | export const FIXTURE_ENTRYPOINT = { + +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). + + 5 | const r = useRef(null); + 6 | const current = !r.current; +> 7 | return
{current}
; + | ^^^^^^^ Cannot access ref value during render + 8 | } + 9 | + 10 | export const FIXTURE_ENTRYPOINT = { +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-access-render-unary.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-access-render-unary.js new file mode 100644 index 0000000000000..8d99008f805b3 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-access-render-unary.js @@ -0,0 +1,13 @@ +//@flow +import {useRef} from 'react'; + +component C() { + const r = useRef(null); + const current = !r.current; + return
{current}
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: C, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-initialization-unary-not.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-initialization-unary-not.expect.md new file mode 100644 index 0000000000000..516d006c205a9 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-initialization-unary-not.expect.md @@ -0,0 +1,43 @@ + +## Input + +```javascript +//@flow +import {useRef} from 'react'; + +component C() { + const r = useRef(null); + if (!r.current) { + r.current = 1; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: C, + params: [{}], +}; + +``` + + +## 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 C() { + 5 | const r = useRef(null); +> 6 | if (!r.current) { + | ^^^^^^^^^ Cannot access ref value during render + 7 | r.current = 1; + 8 | } + 9 | } + +To initialize a ref only once, check that the ref is null with the pattern `if (ref.current == null) { ref.current = ... }` +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-initialization-unary-not.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-initialization-unary-not.js new file mode 100644 index 0000000000000..b9b5d41295cdb --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ref-initialization-unary-not.js @@ -0,0 +1,14 @@ +//@flow +import {useRef} from 'react'; + +component C() { + const r = useRef(null); + if (!r.current) { + r.current = 1; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: C, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-set-state-in-useEffect-from-ref.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-set-state-in-useEffect-from-ref.expect.md new file mode 100644 index 0000000000000..e60888bafc647 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-set-state-in-useEffect-from-ref.expect.md @@ -0,0 +1,63 @@ + +## Input + +```javascript +// @validateNoSetStateInEffects +import {useState, useRef, useEffect} from 'react'; + +function Tooltip() { + const ref = useRef(null); + const [tooltipHeight, setTooltipHeight] = useState(0); + + useEffect(() => { + const {height} = ref.current.getBoundingClientRect(); + setTooltipHeight(height); + }, []); + + return tooltipHeight; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Tooltip, + params: [], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInEffects +import { useState, useRef, useEffect } from "react"; + +function Tooltip() { + const $ = _c(2); + const ref = useRef(null); + const [tooltipHeight, setTooltipHeight] = useState(0); + let t0; + let t1; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = () => { + const { height } = ref.current.getBoundingClientRect(); + setTooltipHeight(height); + }; + t1 = []; + $[0] = t0; + $[1] = t1; + } else { + t0 = $[0]; + t1 = $[1]; + } + useEffect(t0, t1); + return tooltipHeight; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Tooltip, + params: [], +}; + +``` + +### Eval output +(kind: exception) Cannot read properties of null (reading 'getBoundingClientRect') \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-set-state-in-useEffect-from-ref.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-set-state-in-useEffect-from-ref.js new file mode 100644 index 0000000000000..e27292c0a8a85 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-set-state-in-useEffect-from-ref.js @@ -0,0 +1,19 @@ +// @validateNoSetStateInEffects +import {useState, useRef, useEffect} from 'react'; + +function Tooltip() { + const ref = useRef(null); + const [tooltipHeight, setTooltipHeight] = useState(0); + + useEffect(() => { + const {height} = ref.current.getBoundingClientRect(); + setTooltipHeight(height); + }, []); + + return tooltipHeight; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Tooltip, + params: [], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-effect-from-ref-arithmetic.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-effect-from-ref-arithmetic.expect.md new file mode 100644 index 0000000000000..ee19358f3fb0e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-effect-from-ref-arithmetic.expect.md @@ -0,0 +1,68 @@ + +## Input + +```javascript +// @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects +import {useState, useRef, useLayoutEffect} from 'react'; + +function Component() { + const ref = useRef({size: 5}); + const [computedSize, setComputedSize] = useState(0); + + useLayoutEffect(() => { + setComputedSize(ref.current.size * 10); + }, []); + + return computedSize; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects +import { useState, useRef, useLayoutEffect } from "react"; + +function Component() { + const $ = _c(3); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = { size: 5 }; + $[0] = t0; + } else { + t0 = $[0]; + } + const ref = useRef(t0); + const [computedSize, setComputedSize] = useState(0); + let t1; + let t2; + if ($[1] === Symbol.for("react.memo_cache_sentinel")) { + t1 = () => { + setComputedSize(ref.current.size * 10); + }; + t2 = []; + $[1] = t1; + $[2] = t2; + } else { + t1 = $[1]; + t2 = $[2]; + } + useLayoutEffect(t1, t2); + return computedSize; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], +}; + +``` + +### Eval output +(kind: ok) 50 \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-effect-from-ref-arithmetic.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-effect-from-ref-arithmetic.js new file mode 100644 index 0000000000000..d312b139b7012 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-effect-from-ref-arithmetic.js @@ -0,0 +1,18 @@ +// @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects +import {useState, useRef, useLayoutEffect} from 'react'; + +function Component() { + const ref = useRef({size: 5}); + const [computedSize, setComputedSize] = useState(0); + + useLayoutEffect(() => { + setComputedSize(ref.current.size * 10); + }, []); + + return computedSize; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-effect-from-ref-array-index.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-effect-from-ref-array-index.expect.md new file mode 100644 index 0000000000000..a7c3714b18f87 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-effect-from-ref-array-index.expect.md @@ -0,0 +1,69 @@ + +## Input + +```javascript +// @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects +import {useState, useRef, useEffect} from 'react'; + +function Component() { + const ref = useRef([1, 2, 3, 4, 5]); + const [value, setValue] = useState(0); + + useEffect(() => { + const index = 2; + setValue(ref.current[index]); + }, []); + + return value; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects +import { useState, useRef, useEffect } from "react"; + +function Component() { + const $ = _c(3); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = [1, 2, 3, 4, 5]; + $[0] = t0; + } else { + t0 = $[0]; + } + const ref = useRef(t0); + const [value, setValue] = useState(0); + let t1; + let t2; + if ($[1] === Symbol.for("react.memo_cache_sentinel")) { + t1 = () => { + setValue(ref.current[2]); + }; + t2 = []; + $[1] = t1; + $[2] = t2; + } else { + t1 = $[1]; + t2 = $[2]; + } + useEffect(t1, t2); + return value; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], +}; + +``` + +### Eval output +(kind: ok) 3 \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-effect-from-ref-array-index.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-effect-from-ref-array-index.js new file mode 100644 index 0000000000000..90459ac445cf1 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-effect-from-ref-array-index.js @@ -0,0 +1,19 @@ +// @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects +import {useState, useRef, useEffect} from 'react'; + +function Component() { + const ref = useRef([1, 2, 3, 4, 5]); + const [value, setValue] = useState(0); + + useEffect(() => { + const index = 2; + setValue(ref.current[index]); + }, []); + + return value; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-effect-from-ref-function-call.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-effect-from-ref-function-call.expect.md new file mode 100644 index 0000000000000..8a46686fcde25 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-effect-from-ref-function-call.expect.md @@ -0,0 +1,75 @@ + +## Input + +```javascript +// @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects +import {useState, useRef, useEffect} from 'react'; + +function Component() { + const ref = useRef(null); + const [width, setWidth] = useState(0); + + useEffect(() => { + function getBoundingRect(ref) { + if (ref.current) { + return ref.current.getBoundingClientRect?.()?.width ?? 100; + } + return 100; + } + + setWidth(getBoundingRect(ref)); + }, []); + + return width; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects +import { useState, useRef, useEffect } from "react"; + +function Component() { + const $ = _c(2); + const ref = useRef(null); + const [width, setWidth] = useState(0); + let t0; + let t1; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = () => { + const getBoundingRect = function getBoundingRect(ref_0) { + if (ref_0.current) { + return ref_0.current.getBoundingClientRect?.()?.width ?? 100; + } + return 100; + }; + + setWidth(getBoundingRect(ref)); + }; + t1 = []; + $[0] = t0; + $[1] = t1; + } else { + t0 = $[0]; + t1 = $[1]; + } + useEffect(t0, t1); + return width; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], +}; + +``` + +### Eval output +(kind: ok) 100 \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-effect-from-ref-function-call.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-effect-from-ref-function-call.js new file mode 100644 index 0000000000000..e37b3f3ea2b4b --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-effect-from-ref-function-call.js @@ -0,0 +1,25 @@ +// @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects +import {useState, useRef, useEffect} from 'react'; + +function Component() { + const ref = useRef(null); + const [width, setWidth] = useState(0); + + useEffect(() => { + function getBoundingRect(ref) { + if (ref.current) { + return ref.current.getBoundingClientRect?.()?.width ?? 100; + } + return 100; + } + + setWidth(getBoundingRect(ref)); + }, []); + + return width; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useLayoutEffect-from-ref.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useLayoutEffect-from-ref.expect.md new file mode 100644 index 0000000000000..cacd46bfe42a6 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useLayoutEffect-from-ref.expect.md @@ -0,0 +1,63 @@ + +## Input + +```javascript +// @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects +import {useState, useRef, useLayoutEffect} from 'react'; + +function Tooltip() { + const ref = useRef(null); + const [tooltipHeight, setTooltipHeight] = useState(0); + + useLayoutEffect(() => { + const {height} = ref.current.getBoundingClientRect(); + setTooltipHeight(height); + }, []); + + return tooltipHeight; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Tooltip, + params: [], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects +import { useState, useRef, useLayoutEffect } from "react"; + +function Tooltip() { + const $ = _c(2); + const ref = useRef(null); + const [tooltipHeight, setTooltipHeight] = useState(0); + let t0; + let t1; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = () => { + const { height } = ref.current.getBoundingClientRect(); + setTooltipHeight(height); + }; + t1 = []; + $[0] = t0; + $[1] = t1; + } else { + t0 = $[0]; + t1 = $[1]; + } + useLayoutEffect(t0, t1); + return tooltipHeight; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Tooltip, + params: [], +}; + +``` + +### Eval output +(kind: exception) Cannot read properties of null (reading 'getBoundingClientRect') \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useLayoutEffect-from-ref.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useLayoutEffect-from-ref.js new file mode 100644 index 0000000000000..339b550730d77 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/valid-setState-in-useLayoutEffect-from-ref.js @@ -0,0 +1,19 @@ +// @validateNoSetStateInEffects @enableAllowSetStateFromRefsInEffects +import {useState, useRef, useLayoutEffect} from 'react'; + +function Tooltip() { + const ref = useRef(null); + const [tooltipHeight, setTooltipHeight] = useState(0); + + useLayoutEffect(() => { + const {height} = ref.current.getBoundingClientRect(); + setTooltipHeight(height); + }, []); + + return tooltipHeight; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Tooltip, + params: [], +};