/
ashipov
/
react
Обзор
Документация
Войти
/
ashipov
/
react
Код
Запросы
0
Задачи 2.0
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateNoSetStateInRender.ts
201 строка
8 KB
lauren
[compiler] Add enableUseKeyedState flag and improve setState-in-render errors (#35230)
05 дек 2025, 02:29
Не верифицирован
05 дек 2025, 02:29
f99241b
Код
Авторство
О чём код?
/** * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import { CompilerDiagnostic, CompilerError, ErrorCategory, } from '../CompilerError'; import {HIRFunction, IdentifierId, isSetStateType} from '../HIR'; import {computeUnconditionalBlocks} from '../HIR/ComputeUnconditionalBlocks'; import {eachInstructionValueOperand} from '../HIR/visitors'; import {Result} from '../Utils/Result'; /** * Validates that the given function does not have an infinite update loop * caused by unconditionally calling setState during render. This validation * is conservative and cannot catch all cases of unconditional setState in * render, but avoids false positives. Examples of cases that are caught: * * ```javascript * // Direct call of setState: * const [state, setState] = useState(false); * setState(true); * * // Indirect via a function: * const [state, setState] = useState(false); * const setTrue = () => setState(true); * setTrue(); * ``` * * However, storing setState inside another value and accessing it is not yet * validated: * * ``` * // false negative, not detected but will cause an infinite render loop * const [state, setState] = useState(false); * const x = [setState]; * const y = x.pop(); * y(); * ``` */ export function validateNoSetStateInRender( fn: HIRFunction, ): Result<void, CompilerError> { const unconditionalSetStateFunctions: Set<IdentifierId> = new Set(); return validateNoSetStateInRenderImpl(fn, unconditionalSetStateFunctions); } function validateNoSetStateInRenderImpl( fn: HIRFunction, unconditionalSetStateFunctions: Set<IdentifierId>, ): Result<void, CompilerError> { const unconditionalBlocks = computeUnconditionalBlocks(fn); let activeManualMemoId: number | null = null; const errors = new CompilerError(); for (const [, block] of fn.body.blocks) { for (const instr of block.instructions) { switch (instr.value.kind) { case 'LoadLocal': { if ( unconditionalSetStateFunctions.has(instr.value.place.identifier.id) ) { unconditionalSetStateFunctions.add(instr.lvalue.identifier.id); } break; } case 'StoreLocal': { if ( unconditionalSetStateFunctions.has(instr.value.value.identifier.id) ) { unconditionalSetStateFunctions.add( instr.value.lvalue.place.identifier.id, ); unconditionalSetStateFunctions.add(instr.lvalue.identifier.id); } break; } case 'ObjectMethod': case 'FunctionExpression': { if ( // faster-path to check if the function expression references a setState [...eachInstructionValueOperand(instr.value)].some( operand => isSetStateType(operand.identifier) || unconditionalSetStateFunctions.has(operand.identifier.id), ) && // if yes, does it unconditonally call it? validateNoSetStateInRenderImpl( instr.value.loweredFunc.func, unconditionalSetStateFunctions, ).isErr() ) { // This function expression unconditionally calls a setState unconditionalSetStateFunctions.add(instr.lvalue.identifier.id); } break; } case 'StartMemoize': { CompilerError.invariant(activeManualMemoId === null, { reason: 'Unexpected nested StartMemoize instructions', description: null, details: [ { kind: 'error', loc: instr.value.loc, message: null, }, ], }); activeManualMemoId = instr.value.manualMemoId; break; } case 'FinishMemoize': { CompilerError.invariant( activeManualMemoId === instr.value.manualMemoId, { reason: 'Expected FinishMemoize to align with previous StartMemoize instruction', description: null, details: [ { kind: 'error', loc: instr.value.loc, message: null, }, ], }, ); activeManualMemoId = null; break; } case 'CallExpression': { const callee = instr.value.callee; if ( isSetStateType(callee.identifier) || unconditionalSetStateFunctions.has(callee.identifier.id) ) { if (activeManualMemoId !== null) { errors.pushDiagnostic( CompilerDiagnostic.create({ category: ErrorCategory.RenderSetState, reason: 'Calling setState from useMemo may trigger an infinite loop', description: 'Each time the memo callback is evaluated it will change state. This can cause a memoization dependency to change, running the memo function again and causing an infinite loop. Instead of setting state in useMemo(), prefer deriving the value during render. (https://react.dev/reference/react/useState)', suggestions: null, }).withDetails({ kind: 'error', loc: callee.loc, message: 'Found setState() within useMemo()', }), ); } else if (unconditionalBlocks.has(block.id)) { const enableUseKeyedState = fn.env.config.enableUseKeyedState; if (enableUseKeyedState) { errors.pushDiagnostic( CompilerDiagnostic.create({ category: ErrorCategory.RenderSetState, reason: 'Cannot call setState during render', description: 'Calling setState during render may trigger an infinite loop.\n' + '* To reset state when other state/props change, use `const [state, setState] = useKeyedState(initialState, key)` to reset `state` when `key` changes.\n' + '* To derive data from other state/props, compute the derived data during render without using state', suggestions: null, }).withDetails({ kind: 'error', loc: callee.loc, message: 'Found setState() in render', }), ); } else { errors.pushDiagnostic( CompilerDiagnostic.create({ category: ErrorCategory.RenderSetState, reason: 'Cannot call setState during render', description: 'Calling setState during render may trigger an infinite loop.\n' + '* To reset state when other state/props change, store the previous value in state and update conditionally: https://react.dev/reference/react/useState#storing-information-from-previous-renders\n' + '* To derive data from other state/props, compute the derived data during render without using state', suggestions: null, }).withDetails({ kind: 'error', loc: callee.loc, message: 'Found setState() in render', }), ); } } } break; } } } } return errors.asResult(); }