Repository: facebook/react
Pull request: [lint] Use settings for additional hooks in exhaustive deps



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

Changed files:
filename: packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js
patch: @@ -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() {

filename: packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts
patch: @@ -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\``,
         });
       }