From 2b70e33509d7dfd4840a99851807b53a94e2ef20 Mon Sep 17 00:00:00 2001 From: Andrii Furmanets Date: Sun, 28 Dec 2025 11:15:03 +0200 Subject: [PATCH] fix(eslint-plugin-react-hooks): report hooks in default export arrow functions --- .../__tests__/ESLintRulesOfHooks-test.js | 25 +++++++++---------- .../src/rules/RulesOfHooks.ts | 14 ++++++++++- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js index 3d60a36824d..3a9b7da793f 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js +++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js @@ -547,17 +547,6 @@ const allTests = { // TODO: this should error but doesn't. // errors: [functionError('use', 'notAComponent')], }, - { - code: normalizeIndent` - export default () => { - if (isVal) { - useState(0); - } - } - `, - // TODO: this should error but doesn't. - // errors: [genericError('useState')], - }, { code: normalizeIndent` function notAComponent() { @@ -615,7 +604,7 @@ const allTests = { onClick(); }); useServerEffect(() => { - onClick(); + onClick(); }); } `, @@ -845,7 +834,7 @@ const allTests = { }, { code: normalizeIndent` - // Valid because functions created with useEffectEvent can be passed by reference in useLayoutEffect + // Valid because functions created with useEffectEvent can be passed by reference in useLayoutEffect // and useInsertionEffect. function MyComponent({ theme }) { const onClick = useEffectEvent(() => { @@ -915,6 +904,16 @@ const allTests = { }, ], invalid: [ + { + code: normalizeIndent` + export default () => { + if (isVal) { + useState(0); + } + } + `, + errors: [functionError('useState', 'default')], + }, { syntax: 'flow', code: normalizeIndent` diff --git a/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts b/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts index ca82c99e2f5..c604b14a822 100644 --- a/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts +++ b/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts @@ -701,9 +701,14 @@ const rule = { context.report({node: hook, message}); } else if (codePathFunctionName) { // Custom message if we found an invalid function name. + // Handle both real AST nodes and synthetic identifiers (e.g., for default exports) + const functionNameText = + 'name' in codePathFunctionName && typeof codePathFunctionName.name === 'string' + ? codePathFunctionName.name + : getSourceCode().getText(codePathFunctionName); const message = `React Hook "${getSourceCode().getText(hook)}" is called in ` + - `function "${getSourceCode().getText(codePathFunctionName)}" ` + + `function "${functionNameText}" ` + 'that is neither a React function component nor a custom ' + 'React Hook function.' + ' React component names must start with an uppercase letter.' + @@ -916,6 +921,13 @@ function getFunctionName(node: Node) { // Kinda clowny, but we'd said we'd follow spec convention for // `IsAnonymousFunctionDefinition()` usage. return node.parent.left; + } else if (node.parent?.type === 'ExportDefaultDeclaration') { + // export default () => {}; + // export default function() {}; + // + // For default exports, we use a synthetic identifier to represent + // the "default" export name, which is not a valid component name. + return {type: 'Identifier', name: 'default'} as Node; } else { return undefined; }