From f339d970f487b39e4774a99814d8fcdaa4b7b283 Mon Sep 17 00:00:00 2001 From: Ousama Ben Younes Date: Fri, 17 Apr 2026 06:30:16 +0000 Subject: [PATCH] [eslint-plugin-react-hooks] Allow underscore-prefixed component names `isComponentName` required the identifier to start with a capital letter, which flagged `_ComponentName` (a common private-naming convention for "unexported / not-for-external-use" components) as neither a component nor a hook. Users then got a confusing `rules-of-hooks` error inside what is clearly a component. Widen the regex to `/^_?[A-Z]/` so a single leading underscore is permitted so long as it is followed by an uppercase letter. Purely lowercase names (including `_notAComponent`) are still rejected (fixes #31722). Co-Authored-By: Claude --- .../__tests__/ESLintRulesOfHooks-test.js | 21 +++++++++++++++++++ .../src/rules/RulesOfHooks.ts | 6 ++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js index 3d60a36824d2..6761419453f3 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js +++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js @@ -45,6 +45,16 @@ const allTests = { } `, }, + { + code: normalizeIndent` + // Valid because a single leading underscore is a private-naming + // convention; what follows still starts with an uppercase letter, + // so this is recognized as a component. See facebook/react#31722. + function _ComponentWithHook() { + useHook(); + } + `, + }, { syntax: 'flow', code: normalizeIndent` @@ -1114,6 +1124,17 @@ const allTests = { `, errors: [functionError('useState', 'handleClick')], }, + { + code: normalizeIndent` + // Invalid because the underscore-prefix exemption still requires + // an uppercase letter afterwards (so lowercase-after-underscore + // names are not recognized as components). + function _notAComponent() { + useHook(); + } + `, + errors: [functionError('useHook', '_notAComponent')], + }, { code: normalizeIndent` // Invalid because it's a common misunderstanding. diff --git a/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts b/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts index ca82c99e2f55..fb470017948f 100644 --- a/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts +++ b/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts @@ -52,10 +52,12 @@ function isHook(node: Node): boolean { /** * Checks if the node is a React component name. React component names must - * always start with an uppercase letter. + * always start with an uppercase letter. A single leading underscore is + * permitted as a private-naming convention (e.g. `_InternalComponent`), so + * long as it is followed by an uppercase letter. */ function isComponentName(node: Node): boolean { - return node.type === 'Identifier' && /^[A-Z]/.test(node.name); + return node.type === 'Identifier' && /^_?[A-Z]/.test(node.name); } function isReactFunction(node: Node, functionName: string): boolean {