From 350ed330088906e994c765b2d35e3287ca05088e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Eirinha?= Date: Sun, 12 Oct 2025 17:59:31 +0100 Subject: [PATCH] Disallow passing useEffectEffect down inlined as a prop --- .../__tests__/ESLintRulesOfHooks-test.js | 19 ++++++++++++++ .../src/rules/RulesOfHooks.ts | 26 ++++++++++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js index 3e89624c6d3f1..05bdb1e71ed8f 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js +++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js @@ -1555,6 +1555,17 @@ const allTests = { `, errors: [useEffectEventError('onClick', false)], }, + { + code: normalizeIndent` + // Invalid because useEffectEvent is being passed down + function MyComponent({ theme }) { + return { + showNotification(theme); + })} />; + } + `, + errors: [{...useEffectEventError(null, false), line: 4}], + }, { code: normalizeIndent` // This should error even though it shares an identifier name with the below @@ -1726,6 +1737,14 @@ function classError(hook) { } function useEffectEventError(fn, called) { + if (fn === null) { + return { + message: + `React Hook "useEffectEvent" can only be called at the top level of your component.` + + ` It cannot be passed down.`, + }; + } + return { message: `\`${fn}\` is a function created with React Hook "useEffectEvent", and can only be called from ` + diff --git a/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts b/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts index ba398d850dedb..4e49e96bfe68c 100644 --- a/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts +++ b/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts @@ -171,7 +171,15 @@ function isUseEffectEventIdentifier(node: Node): boolean { return node.type === 'Identifier' && node.name === 'useEffectEvent'; } -function useEffectEventError(fn: string, called: boolean): string { +function useEffectEventError(fn: string | null, called: boolean): string { + // no function identifier, i.e. it is not assigned to a variable + if (fn === null) { + return ( + `React Hook "useEffectEvent" can only be called at the top level of your component.` + + ` It cannot be passed down.` + ); + } + return ( `\`${fn}\` is a function created with React Hook "useEffectEvent", and can only be called from ` + 'Effects and Effect Events in the same component.' + @@ -772,6 +780,22 @@ const rule = { // comparison later when we exit lastEffect = node; } + + // Specifically disallow because this + // case can't be caught by `recordAllUseEffectEventFunctions` as it isn't assigned to a variable + if ( + isUseEffectEventIdentifier(nodeWithoutNamespace) && + node.parent?.type !== 'VariableDeclarator' && + // like in other hooks, calling useEffectEvent at component's top level without assignment is valid + node.parent?.type !== 'ExpressionStatement' + ) { + const message = useEffectEventError(null, false); + + context.report({ + node, + message, + }); + } }, Identifier(node) {