From 2a04bae6517853aa8c1eb2af1a8eb3e447db1796 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Tue, 30 Sep 2025 16:44:43 -0400 Subject: [PATCH] [lint] Use settings for additional hooks in exhaustive deps (#34637) Like in the diff below, we can read from the shared configuration to check exhaustive deps. I allow the classic additionalHooks configuration to override it so that this change is backwards compatible. -- --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/34637). * __->__ #34637 * #34497 --- .../ESLintRuleExhaustiveDeps-test.js | 98 +++++++++++++++++++ .../src/rules/ExhaustiveDeps.ts | 14 ++- 2 files changed, 108 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js index 812c2010a0..dca94c516c 100644 --- a/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js +++ b/packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js @@ -1485,6 +1485,70 @@ const tests = { } `, }, + { + // Test settings-based additionalHooks - should work with settings + code: normalizeIndent` + function MyComponent(props) { + useCustomEffect(() => { + console.log(props.foo); + }); + } + `, + settings: { + 'react-hooks': { + additionalEffectHooks: 'useCustomEffect', + }, + }, + }, + { + // Test settings-based additionalHooks - should work with dependencies + code: normalizeIndent` + function MyComponent(props) { + useCustomEffect(() => { + console.log(props.foo); + }, [props.foo]); + } + `, + settings: { + 'react-hooks': { + additionalEffectHooks: 'useCustomEffect', + }, + }, + }, + { + // Test that rule-level additionalHooks takes precedence over settings + code: normalizeIndent` + function MyComponent(props) { + useCustomEffect(() => { + console.log(props.foo); + }, []); + } + `, + options: [{additionalHooks: 'useAnotherEffect'}], + settings: { + 'react-hooks': { + additionalEffectHooks: 'useCustomEffect', + }, + }, + }, + { + // Test settings with multiple hooks pattern + code: normalizeIndent` + function MyComponent(props) { + useCustomEffect(() => { + console.log(props.foo); + }, [props.foo]); + useAnotherEffect(() => { + console.log(props.bar); + }, [props.bar]); + } + `, + settings: { + 'react-hooks': { + additionalEffectHooks: '(useCustomEffect|useAnotherEffect)', + }, + }, + }, ], invalid: [ { @@ -3714,6 +3778,40 @@ const tests = { }, ], }, + { + // Test settings-based additionalHooks - should detect missing dependency + code: normalizeIndent` + function MyComponent(props) { + useCustomEffect(() => { + console.log(props.foo); + }, []); + } + `, + settings: { + 'react-hooks': { + additionalEffectHooks: 'useCustomEffect', + }, + }, + errors: [ + { + message: + "React Hook useCustomEffect has a missing dependency: 'props.foo'. " + + 'Either include it or remove the dependency array.', + suggestions: [ + { + desc: 'Update the dependencies array to be: [props.foo]', + output: normalizeIndent` + function MyComponent(props) { + useCustomEffect(() => { + console.log(props.foo); + }, [props.foo]); + } + `, + }, + ], + }, + ], + }, { code: normalizeIndent` function MyComponent() { diff --git a/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts b/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts index d59a1ff792..8523c3cc2e 100644 --- a/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts +++ b/packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts @@ -21,6 +21,8 @@ import type { VariableDeclarator, } from 'estree'; +import { getAdditionalEffectHooksFromSettings } from '../shared/Utils'; + type DeclaredDependency = { key: string; node: Node; @@ -69,19 +71,22 @@ const rule = { }, requireExplicitEffectDeps: { type: 'boolean', - } + }, }, }, ], }, create(context: Rule.RuleContext) { const rawOptions = context.options && context.options[0]; + const settings = context.settings || {}; + // Parse the `additionalHooks` regex. + // Use rule-level additionalHooks if provided, otherwise fall back to settings const additionalHooks = rawOptions && rawOptions.additionalHooks ? new RegExp(rawOptions.additionalHooks) - : undefined; + : getAdditionalEffectHooksFromSettings(settings); const enableDangerousAutofixThisMayCauseInfiniteLoops: boolean = (rawOptions && @@ -93,7 +98,8 @@ const rule = { ? rawOptions.experimental_autoDependenciesHooks : []; - const requireExplicitEffectDeps: boolean = rawOptions && rawOptions.requireExplicitEffectDeps || false; + const requireExplicitEffectDeps: boolean = + (rawOptions && rawOptions.requireExplicitEffectDeps) || false; const options = { additionalHooks, @@ -1351,7 +1357,7 @@ const rule = { node: reactiveHook, message: `React Hook ${reactiveHookName} always requires dependencies. ` + - `Please add a dependency array or an explicit \`undefined\`` + `Please add a dependency array or an explicit \`undefined\``, }); }