From e5c315a3c17692e9b701648e82e4da994b63712f Mon Sep 17 00:00:00 2001 From: Raashish Aggarwal <94279692+raashish1601@users.noreply.github.com> Date: Sat, 2 May 2026 11:00:26 +0530 Subject: [PATCH] Recognize symbol-only hook names --- .../babel-plugin-annotate-react-code.ts | 2 +- .../src/Entrypoint/Program.ts | 2 +- .../src/HIR/Environment.ts | 4 +- ...nditional-call-hook-symbol-names.expect.md | 46 +++++++++++++++++++ ...alid-conditional-call-hook-symbol-names.js | 11 +++++ .../__tests__/ESLintRulesOfHooks-test.js | 7 +++ .../src/rules/RulesOfHooks.ts | 6 ++- 7 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-call-hook-symbol-names.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-call-hook-symbol-names.js diff --git a/compiler/packages/babel-plugin-react-compiler/scripts/babel-plugin-annotate-react-code.ts b/compiler/packages/babel-plugin-react-compiler/scripts/babel-plugin-annotate-react-code.ts index 4970e39f2e62..020437047355 100644 --- a/compiler/packages/babel-plugin-react-compiler/scripts/babel-plugin-annotate-react-code.ts +++ b/compiler/packages/babel-plugin-react-compiler/scripts/babel-plugin-annotate-react-code.ts @@ -137,7 +137,7 @@ function isComponentOrHookLike( } function isHookName(s: string): boolean { - return /^use[A-Z0-9]/.test(s); + return /^use[A-Z0-9]/.test(s) || s === 'use$' || s === 'use_'; } /* diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts index 2880e9283c77..11a57173bfe3 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Program.ts @@ -895,7 +895,7 @@ function hasMemoCacheFunctionImport( } function isHookName(s: string): boolean { - return /^use[A-Z0-9]/.test(s); + return /^use[A-Z0-9]/.test(s) || s === 'use$' || s === 'use_'; } /* diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts index 98cf1ed57d9f..86e91b6cd735 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -1033,9 +1033,9 @@ export class Environment { const REANIMATED_MODULE_NAME = 'react-native-reanimated'; -// From https://github.com/facebook/react/blob/main/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js#LL18C1-L23C2 +// From packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts export function isHookName(name: string): boolean { - return /^use[A-Z0-9]/.test(name); + return /^use[A-Z0-9]/.test(name) || name === 'use$' || name === 'use_'; } export function parseEnvironmentConfig( diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-call-hook-symbol-names.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-call-hook-symbol-names.expect.md new file mode 100644 index 000000000000..e35a9d4af97d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-call-hook-symbol-names.expect.md @@ -0,0 +1,46 @@ + +## Input + +```javascript +import {use$, use_} from 'shared-runtime'; + +function Component(props) { + if (props.cond) { + use$(); + } + if (props.otherCond) { + use_(); + } + return null; +} + +``` + + +## Error + +``` +Found 2 errors: + +Error: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) + +error.invalid-conditional-call-hook-symbol-names.ts:5:4 + 3 | function Component(props) { + 4 | if (props.cond) { +> 5 | use$(); + | ^^^^ Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) + 6 | } + 7 | if (props.otherCond) { + 8 | use_(); + +Error: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) + +error.invalid-conditional-call-hook-symbol-names.ts:8:4 + 6 | } + 7 | if (props.otherCond) { +> 8 | use_(); + | ^^^^ Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning) + 9 | } + 10 | return null; + 11 | } +``` diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-call-hook-symbol-names.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-call-hook-symbol-names.js new file mode 100644 index 000000000000..204f4623cb18 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-conditional-call-hook-symbol-names.js @@ -0,0 +1,11 @@ +import {use$, use_} from 'shared-runtime'; + +function Component(props) { + if (props.cond) { + use$(); + } + if (props.otherCond) { + use_(); + } + return null; +} diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js index 3d60a36824d2..f35d108b5425 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js +++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRulesOfHooks-test.js @@ -153,6 +153,13 @@ const allTests = { Namespace.useHook = () => { useState(); }; `, }, + { + code: normalizeIndent` + // Valid because hook names may use JavaScript identifier symbols. + function use$() { useState(); } + function use_() { useState(); } + `, + }, { code: normalizeIndent` // Valid because hooks can call hooks. diff --git a/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts b/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts index ca82c99e2f55..8ef81ee98805 100644 --- a/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts +++ b/packages/eslint-plugin-react-hooks/src/rules/RulesOfHooks.ts @@ -24,10 +24,12 @@ import {getAdditionalEffectHooksFromSettings} from '../shared/Utils'; /** * Catch all identifiers that begin with "use" followed by an uppercase Latin - * character to exclude identifiers like "user". + * character or number, or the symbol-only hook names use$ and use_, to exclude + * identifiers like "user" and preserve existing behavior for names like + * "use_hook". */ function isHookName(s: string): boolean { - return s === 'use' || /^use[A-Z0-9]/.test(s); + return s === 'use' || /^use[A-Z0-9]/.test(s) || s === 'use$' || s === 'use_'; } /**