[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
Jordan Brown committed
Sep 30, 2025 at 16:44 UTC
2a04bae6517853aa8c1eb2af1a8eb3e447db1796
2 files changed
+108
-4
packages/eslint-plugin-react-hooks/__tests__/ESLintRuleExhaustiveDeps-test.js
+98
@@ -1485,6 +1485,70 @@ const tests = {
1485
}
1486
`,
1487
},
1488
+ {
1489
+ // Test settings-based additionalHooks - should work with settings
1490
+ code: normalizeIndent`
1491
+ function MyComponent(props) {
1492
+ useCustomEffect(() => {
1493
+ console.log(props.foo);
1494
+ });
1495
+ }
1496
+ `,
1497
+ settings: {
1498
+ 'react-hooks': {
1499
+ additionalEffectHooks: 'useCustomEffect',
1500
+ },
1501
+ },
1502
+ },
1503
+ {
1504
+ // Test settings-based additionalHooks - should work with dependencies
1505
+ code: normalizeIndent`
1506
+ function MyComponent(props) {
1507
+ useCustomEffect(() => {
1508
+ console.log(props.foo);
1509
+ }, [props.foo]);
1510
+ }
1511
+ `,
1512
+ settings: {
1513
+ 'react-hooks': {
1514
+ additionalEffectHooks: 'useCustomEffect',
1515
+ },
1516
+ },
1517
+ },
1518
+ {
1519
+ // Test that rule-level additionalHooks takes precedence over settings
1520
+ code: normalizeIndent`
1521
+ function MyComponent(props) {
1522
+ useCustomEffect(() => {
1523
+ console.log(props.foo);
1524
+ }, []);
1525
+ }
1526
+ `,
1527
+ options: [{additionalHooks: 'useAnotherEffect'}],
1528
+ settings: {
1529
+ 'react-hooks': {
1530
+ additionalEffectHooks: 'useCustomEffect',
1531
+ },
1532
+ },
1533
+ },
1534
+ {
1535
+ // Test settings with multiple hooks pattern
1536
+ code: normalizeIndent`
1537
+ function MyComponent(props) {
1538
+ useCustomEffect(() => {
1539
+ console.log(props.foo);
1540
+ }, [props.foo]);
1541
+ useAnotherEffect(() => {
1542
+ console.log(props.bar);
1543
+ }, [props.bar]);
1544
+ }
1545
+ `,
1546
+ settings: {
1547
+ 'react-hooks': {
1548
+ additionalEffectHooks: '(useCustomEffect|useAnotherEffect)',
1549
+ },
1550
+ },
1551
+ },
1552
],
1553
invalid: [
1554
{
@@ -3714,6 +3778,40 @@ const tests = {
3778
},
3779
],
3780
},
3781
+ {
3782
+ // Test settings-based additionalHooks - should detect missing dependency
3783
+ code: normalizeIndent`
3784
+ function MyComponent(props) {
3785
+ useCustomEffect(() => {
3786
+ console.log(props.foo);
3787
+ }, []);
3788
+ }
3789
+ `,
3790
+ settings: {
3791
+ 'react-hooks': {
3792
+ additionalEffectHooks: 'useCustomEffect',
3793
+ },
3794
+ },
3795
+ errors: [
3796
+ {
3797
+ message:
3798
+ "React Hook useCustomEffect has a missing dependency: 'props.foo'. " +
3799
+ 'Either include it or remove the dependency array.',
3800
+ suggestions: [
3801
+ {
3802
+ desc: 'Update the dependencies array to be: [props.foo]',
3803
+ output: normalizeIndent`
3804
+ function MyComponent(props) {
3805
+ useCustomEffect(() => {
3806
+ console.log(props.foo);
3807
+ }, [props.foo]);
3808
+ }
3809
+ `,
3810
+ },
3811
+ ],
3812
+ },
3813
+ ],
3814
+ },
3815
{
3816
code: normalizeIndent`
3817
function MyComponent() {
packages/eslint-plugin-react-hooks/src/rules/ExhaustiveDeps.ts
+10
-4
@@ -21,6 +21,8 @@ import type {
21
VariableDeclarator,
22
} from 'estree';
23
24
+import { getAdditionalEffectHooksFromSettings } from '../shared/Utils';
25
+
26
type DeclaredDependency = {
27
key: string;
28
node: Node;
@@ -69,19 +71,22 @@ const rule = {
71
},
72
requireExplicitEffectDeps: {
73
type: 'boolean',
72
- }
74
+ },
75
},
76
},
77
],
78
},
79
create(context: Rule.RuleContext) {
80
const rawOptions = context.options && context.options[0];
81
+ const settings = context.settings || {};
82
+
83
84
// Parse the `additionalHooks` regex.
85
+ // Use rule-level additionalHooks if provided, otherwise fall back to settings
86
const additionalHooks =
87
rawOptions && rawOptions.additionalHooks
88
? new RegExp(rawOptions.additionalHooks)
84
- : undefined;
89
+ : getAdditionalEffectHooksFromSettings(settings);
90
91
const enableDangerousAutofixThisMayCauseInfiniteLoops: boolean =
92
(rawOptions &&
@@ -93,7 +98,8 @@ const rule = {
98
? rawOptions.experimental_autoDependenciesHooks
99
: [];
100
96
- const requireExplicitEffectDeps: boolean = rawOptions && rawOptions.requireExplicitEffectDeps || false;
101
+ const requireExplicitEffectDeps: boolean =
102
+ (rawOptions && rawOptions.requireExplicitEffectDeps) || false;
103
104
const options = {
105
additionalHooks,
@@ -1351,7 +1357,7 @@ const rule = {
1357
node: reactiveHook,
1358
message:
1359
`React Hook ${reactiveHookName} always requires dependencies. ` +
1354
- `Please add a dependency array or an explicit \`undefined\``
1360
+ `Please add a dependency array or an explicit \`undefined\``,
1361
});
1362
}
1363