From 7f81ba050826e5cde01ec3a70861e6efb169c59c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C2=ABSergei?= Date: Wed, 13 May 2026 12:57:37 +0300 Subject: [PATCH] Fix recursive useEffectEvent inference --- .../Inference/InferMutationAliasingEffects.ts | 121 ++++++++++++++++-- .../allow-recursive-useEffectEvent.expect.md | 87 +++++++++++++ .../allow-recursive-useEffectEvent.tsx | 19 +++ 3 files changed, 219 insertions(+), 8 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-recursive-useEffectEvent.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-recursive-useEffectEvent.tsx diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts index 4cdbb6aea6c1..bdf33dedd9eb 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutationAliasingEffects.ts @@ -227,9 +227,29 @@ function findHoistedContextDeclarations( fn: HIRFunction, ): Map { const hoisted = new Map(); + for (const block of fn.body.blocks.values()) { + for (const instr of block.instructions) { + if (instr.value.kind === 'DeclareContext') { + const kind = instr.value.lvalue.kind; + if ( + kind == InstructionKind.HoistedConst || + kind == InstructionKind.HoistedFunction || + kind == InstructionKind.HoistedLet + ) { + hoisted.set(instr.value.lvalue.place.identifier.declarationId, null); + } + } + } + } + const safeSelfReferences = findSelfReferentialEffectEventInitializers( + fn, + hoisted, + ); + const initialized = new Set(); function visit(place: Place): void { if ( hoisted.has(place.identifier.declarationId) && + !initialized.has(place.identifier.declarationId) && hoisted.get(place.identifier.declarationId) == null ) { // If this is the first load of the value, store the location @@ -238,14 +258,24 @@ function findHoistedContextDeclarations( } for (const block of fn.body.blocks.values()) { for (const instr of block.instructions) { - if (instr.value.kind === 'DeclareContext') { - const kind = instr.value.lvalue.kind; - if ( - kind == InstructionKind.HoistedConst || - kind == InstructionKind.HoistedFunction || - kind == InstructionKind.HoistedLet - ) { - hoisted.set(instr.value.lvalue.place.identifier.declarationId, null); + if (instr.value.kind === 'StoreContext') { + visit(instr.value.value); + if (instr.value.lvalue.kind === InstructionKind.Const) { + initialized.add(instr.value.lvalue.place.identifier.declarationId); + } + } else if ( + instr.value.kind === 'FunctionExpression' || + instr.value.kind === 'ObjectMethod' + ) { + for (const operand of eachInstructionValueOperand(instr.value)) { + if ( + safeSelfReferences + .get(instr.lvalue.identifier.id) + ?.has(operand.identifier.declarationId) + ) { + continue; + } + visit(operand); } } else { for (const operand of eachInstructionValueOperand(instr.value)) { @@ -257,9 +287,84 @@ function findHoistedContextDeclarations( visit(operand); } } + for (const [declaration, place] of hoisted) { + if (place === null) { + hoisted.delete(declaration); + } + } return hoisted; } +function findSelfReferentialEffectEventInitializers( + fn: HIRFunction, + hoisted: Map, +): Map> { + const functionCapturesHoisted = new Map>(); + const effectEventCallCaptures = new Map< + IdentifierId, + Map> + >(); + const safeSelfReferences = new Map>(); + + for (const block of fn.body.blocks.values()) { + for (const instr of block.instructions) { + const {value} = instr; + if ( + value.kind === 'FunctionExpression' || + value.kind === 'ObjectMethod' + ) { + for (const operand of eachInstructionValueOperand(value)) { + if (hoisted.has(operand.identifier.declarationId)) { + getOrInsertDefault( + functionCapturesHoisted, + instr.lvalue.identifier.id, + new Set(), + ).add(operand.identifier.declarationId); + } + } + } else if (value.kind === 'CallExpression') { + const hookKind = getHookKind(fn.env, value.callee.identifier); + if (hookKind === 'useEffectEvent') { + const captures = new Map>(); + for (const arg of value.args) { + if (arg.kind !== 'Identifier') { + continue; + } + const captured = functionCapturesHoisted.get(arg.identifier.id); + if (captured == null) { + continue; + } + for (const declaration of captured) { + getOrInsertDefault(captures, declaration, new Set()).add( + arg.identifier.id, + ); + } + } + if (captures.size !== 0) { + effectEventCallCaptures.set(instr.lvalue.identifier.id, captures); + } + } + } else if ( + value.kind === 'StoreContext' && + value.lvalue.kind === InstructionKind.Const + ) { + const declaration = value.lvalue.place.identifier.declarationId; + const captures = effectEventCallCaptures.get(value.value.identifier.id); + const functionIds = captures?.get(declaration); + if (functionIds != null) { + for (const functionId of functionIds) { + getOrInsertDefault(safeSelfReferences, functionId, new Set()).add( + declaration, + ); + } + } + } + } + } + + return safeSelfReferences; +} + class Context { internedEffects: Map = new Map(); instructionSignatureCache: Map = new Map(); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-recursive-useEffectEvent.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-recursive-useEffectEvent.expect.md new file mode 100644 index 000000000000..f26a9046ce10 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/allow-recursive-useEffectEvent.expect.md @@ -0,0 +1,87 @@ + +## Input + +```javascript +import {useEffect, useEffectEvent, useState} from 'react'; + +function TimerBasedComponent(props) { + const repeatEvent = useEffectEvent(() => { + props.onRepeat(); + setTimeout(() => { + repeatEvent(); + }, 60); + }); + + const [down, setDown] = useState(false); + useEffect(() => { + if (down) { + repeatEvent(); + } + }, [down]); + + return