Add reporting modes for react-hooks/exhaustive-effect-dependencies and temporarily enable (#35365)
`react-hooks/exhaustive-effect-dependencies` from `ValidateExhaustiveDeps` reports errors for both missing and extra effect deps. We already have `react-hooks/exhaustive-deps` that errors on missing dependencies. In the future we'd like to consolidate this all to the compiler based error, but for now there's a lot of overlap. Let's enable testing the extra dep warning by splitting out reporting modes. This PR - Creates `on`, `off`, `missing-only`, and `extra-only` reporting modes for the effect dep validation flag - Temporarily enables the new rule with `extra-only` in `eslint-plugin-react-hooks` - Adds additional null checking to `manualMemoLoc` to fix a bug found when running against the fixture
Jack Pope committed
Dec 15, 2025 at 18:59 UTC
88ee1f595572b1dcf8f45897cb115b4bbd1aefb8
17 files changed
+293
-22
compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts
+8
-1
@@ -225,8 +225,15 @@ export const EnvironmentConfigSchema = z.object({
225
226
/**
227
* Validate that dependencies supplied to effect hooks are exhaustive.
228
+ * Can be:
229
+ * - 'off': No validation (default)
230
+ * - 'all': Validate and report both missing and extra dependencies
231
+ * - 'missing-only': Only report missing dependencies
232
+ * - 'extra-only': Only report extra/unnecessary dependencies
233
*/
229
- validateExhaustiveEffectDependencies: z.boolean().default(false),
234
+ validateExhaustiveEffectDependencies: z
235
+ .enum(['off', 'all', 'missing-only', 'extra-only'])
236
+ .default('off'),
237
238
/**
239
* When this is true, rather than pruning existing manual memoization but ensuring or validating
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateExhaustiveDependencies.ts
+29
-6
@@ -141,6 +141,7 @@ export function validateExhaustiveDependencies(
141
reactive,
142
startMemo.depsLoc,
143
ErrorCategory.MemoDependencies,
144
+ 'all',
145
);
146
if (diagnostic != null) {
147
error.pushDiagnostic(diagnostic);
@@ -159,7 +160,7 @@ export function validateExhaustiveDependencies(
160
onStartMemoize,
161
onFinishMemoize,
162
onEffect: (inferred, manual, manualMemoLoc) => {
162
- if (env.config.validateExhaustiveEffectDependencies === false) {
163
+ if (env.config.validateExhaustiveEffectDependencies === 'off') {
164
return;
165
}
166
if (DEBUG) {
@@ -195,12 +196,17 @@ export function validateExhaustiveDependencies(
196
});
197
}
198
}
199
+ const effectReportMode =
200
+ typeof env.config.validateExhaustiveEffectDependencies === 'string'
201
+ ? env.config.validateExhaustiveEffectDependencies
202
+ : 'all';
203
const diagnostic = validateDependencies(
204
Array.from(inferred),
205
manualDeps,
206
reactive,
207
manualMemoLoc,
208
ErrorCategory.EffectExhaustiveDependencies,
209
+ effectReportMode,
210
);
211
if (diagnostic != null) {
212
error.pushDiagnostic(diagnostic);
@@ -220,6 +226,7 @@ function validateDependencies(
226
category:
227
| ErrorCategory.MemoDependencies
228
| ErrorCategory.EffectExhaustiveDependencies,
229
+ exhaustiveDepsReportMode: 'all' | 'missing-only' | 'extra-only',
230
): CompilerDiagnostic | null {
231
// Sort dependencies by name and path, with shorter/non-optional paths first
232
inferred.sort((a, b) => {
@@ -370,9 +377,20 @@ function validateDependencies(
377
extra.push(dep);
378
}
379
373
- if (missing.length !== 0 || extra.length !== 0) {
380
+ // Filter based on report mode
381
+ const filteredMissing =
382
+ exhaustiveDepsReportMode === 'extra-only' ? [] : missing;
383
+ const filteredExtra =
384
+ exhaustiveDepsReportMode === 'missing-only' ? [] : extra;
385
+
386
+ if (filteredMissing.length !== 0 || filteredExtra.length !== 0) {
387
let suggestion: CompilerSuggestion | null = null;
375
- if (manualMemoLoc != null && typeof manualMemoLoc !== 'symbol') {
388
+ if (
389
+ manualMemoLoc != null &&
390
+ typeof manualMemoLoc !== 'symbol' &&
391
+ manualMemoLoc.start.index != null &&
392
+ manualMemoLoc.end.index != null
393
+ ) {
394
suggestion = {
395
description: 'Update dependencies',
396
range: [manualMemoLoc.start.index, manualMemoLoc.end.index],
@@ -388,8 +406,13 @@ function validateDependencies(
406
.join(', ')}]`,
407
};
408
}
391
- const diagnostic = createDiagnostic(category, missing, extra, suggestion);
392
- for (const dep of missing) {
409
+ const diagnostic = createDiagnostic(
410
+ category,
411
+ filteredMissing,
412
+ filteredExtra,
413
+ suggestion,
414
+ );
415
+ for (const dep of filteredMissing) {
416
let reactiveStableValueHint = '';
417
if (isStableType(dep.identifier)) {
418
reactiveStableValueHint =
@@ -402,7 +425,7 @@ function validateDependencies(
425
loc: dep.loc,
426
});
427
}
405
- for (const dep of extra) {
428
+ for (const dep of filteredExtra) {
429
if (dep.root.kind === 'Global') {
430
diagnostic.withDetails({
431
kind: 'error',
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.exhaustive-deps-effect-events.expect.md
+1
-1
@@ -2,7 +2,7 @@
2
## Input
3
4
```javascript
5
-// @validateExhaustiveEffectDependencies
5
+// @validateExhaustiveEffectDependencies:"all"
6
import {useEffect, useEffectEvent} from 'react';
7
8
function Component({x, y, z}) {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.exhaustive-deps-effect-events.js
+1
-1
@@ -1,4 +1,4 @@
1
-// @validateExhaustiveEffectDependencies
1
+// @validateExhaustiveEffectDependencies:"all"
2
import {useEffect, useEffectEvent} from 'react';
3
4
function Component({x, y, z}) {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-effect-deps-extra-only.expect.md
new
+83
@@ -0,0 +1,83 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @validateExhaustiveEffectDependencies:"extra-only"
6
+import {useEffect} from 'react';
7
+
8
+function Component({x, y, z}) {
9
+ // no error: missing dep not reported in extra-only mode
10
+ useEffect(() => {
11
+ log(x);
12
+ }, []);
13
+
14
+ // error: extra dep - y
15
+ useEffect(() => {
16
+ log(x);
17
+ }, [x, y]);
18
+
19
+ // error: extra dep - y (missing dep - z not reported)
20
+ useEffect(() => {
21
+ log(x, z);
22
+ }, [x, y]);
23
+
24
+ // error: extra dep - x.y
25
+ useEffect(() => {
26
+ log(x);
27
+ }, [x.y]);
28
+}
29
+
30
+```
31
+
32
+
33
+## Error
34
+
35
+```
36
+Found 3 errors:
37
+
38
+Error: Found extra effect dependencies
39
+
40
+Extra dependencies can cause an effect to fire more often than it should, resulting in performance problems such as excessive renders and side effects.
41
+
42
+error.invalid-exhaustive-effect-deps-extra-only.ts:13:9
43
+ 11 | useEffect(() => {
44
+ 12 | log(x);
45
+> 13 | }, [x, y]);
46
+ | ^ Unnecessary dependency `y`
47
+ 14 |
48
+ 15 | // error: extra dep - y (missing dep - z not reported)
49
+ 16 | useEffect(() => {
50
+
51
+Inferred dependencies: `[x]`
52
+
53
+Error: Found extra effect dependencies
54
+
55
+Extra dependencies can cause an effect to fire more often than it should, resulting in performance problems such as excessive renders and side effects.
56
+
57
+error.invalid-exhaustive-effect-deps-extra-only.ts:18:9
58
+ 16 | useEffect(() => {
59
+ 17 | log(x, z);
60
+> 18 | }, [x, y]);
61
+ | ^ Unnecessary dependency `y`
62
+ 19 |
63
+ 20 | // error: extra dep - x.y
64
+ 21 | useEffect(() => {
65
+
66
+Inferred dependencies: `[x, z]`
67
+
68
+Error: Found extra effect dependencies
69
+
70
+Extra dependencies can cause an effect to fire more often than it should, resulting in performance problems such as excessive renders and side effects.
71
+
72
+error.invalid-exhaustive-effect-deps-extra-only.ts:23:6
73
+ 21 | useEffect(() => {
74
+ 22 | log(x);
75
+> 23 | }, [x.y]);
76
+ | ^^^ Overly precise dependency `x.y`, use `x` instead
77
+ 24 | }
78
+ 25 |
79
+
80
+Inferred dependencies: `[x]`
81
+```
82
+
83
+
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-effect-deps-extra-only.js
new
+24
@@ -0,0 +1,24 @@
1
+// @validateExhaustiveEffectDependencies:"extra-only"
2
+import {useEffect} from 'react';
3
+
4
+function Component({x, y, z}) {
5
+ // no error: missing dep not reported in extra-only mode
6
+ useEffect(() => {
7
+ log(x);
8
+ }, []);
9
+
10
+ // error: extra dep - y
11
+ useEffect(() => {
12
+ log(x);
13
+ }, [x, y]);
14
+
15
+ // error: extra dep - y (missing dep - z not reported)
16
+ useEffect(() => {
17
+ log(x, z);
18
+ }, [x, y]);
19
+
20
+ // error: extra dep - x.y
21
+ useEffect(() => {
22
+ log(x);
23
+ }, [x.y]);
24
+}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-effect-deps-missing-only.expect.md
new
+84
@@ -0,0 +1,84 @@
1
+
2
+## Input
3
+
4
+```javascript
5
+// @validateExhaustiveEffectDependencies:"missing-only"
6
+import {useEffect} from 'react';
7
+
8
+function Component({x, y, z}) {
9
+ // error: missing dep - x
10
+ useEffect(() => {
11
+ log(x);
12
+ }, []);
13
+
14
+ // no error: extra dep not reported in missing-only mode
15
+ useEffect(() => {
16
+ log(x);
17
+ }, [x, y]);
18
+
19
+ // error: missing dep - z (extra dep - y not reported)
20
+ useEffect(() => {
21
+ log(x, z);
22
+ }, [x, y]);
23
+
24
+ // error: missing dep x
25
+ useEffect(() => {
26
+ log(x);
27
+ }, [x.y]);
28
+}
29
+
30
+```
31
+
32
+
33
+## Error
34
+
35
+```
36
+Found 3 errors:
37
+
38
+Error: Found missing effect dependencies
39
+
40
+Missing dependencies can cause an effect to fire less often than it should.
41
+
42
+error.invalid-exhaustive-effect-deps-missing-only.ts:7:8
43
+ 5 | // error: missing dep - x
44
+ 6 | useEffect(() => {
45
+> 7 | log(x);
46
+ | ^ Missing dependency `x`
47
+ 8 | }, []);
48
+ 9 |
49
+ 10 | // no error: extra dep not reported in missing-only mode
50
+
51
+Inferred dependencies: `[x]`
52
+
53
+Error: Found missing effect dependencies
54
+
55
+Missing dependencies can cause an effect to fire less often than it should.
56
+
57
+error.invalid-exhaustive-effect-deps-missing-only.ts:17:11
58
+ 15 | // error: missing dep - z (extra dep - y not reported)
59
+ 16 | useEffect(() => {
60
+> 17 | log(x, z);
61
+ | ^ Missing dependency `z`
62
+ 18 | }, [x, y]);
63
+ 19 |
64
+ 20 | // error: missing dep x
65
+
66
+Inferred dependencies: `[x, z]`
67
+
68
+Error: Found missing effect dependencies
69
+
70
+Missing dependencies can cause an effect to fire less often than it should.
71
+
72
+error.invalid-exhaustive-effect-deps-missing-only.ts:22:8
73
+ 20 | // error: missing dep x
74
+ 21 | useEffect(() => {
75
+> 22 | log(x);
76
+ | ^ Missing dependency `x`
77
+ 23 | }, [x.y]);
78
+ 24 | }
79
+ 25 |
80
+
81
+Inferred dependencies: `[x]`
82
+```
83
+
84
+
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-effect-deps-missing-only.js
new
+24
@@ -0,0 +1,24 @@
1
+// @validateExhaustiveEffectDependencies:"missing-only"
2
+import {useEffect} from 'react';
3
+
4
+function Component({x, y, z}) {
5
+ // error: missing dep - x
6
+ useEffect(() => {
7
+ log(x);
8
+ }, []);
9
+
10
+ // no error: extra dep not reported in missing-only mode
11
+ useEffect(() => {
12
+ log(x);
13
+ }, [x, y]);
14
+
15
+ // error: missing dep - z (extra dep - y not reported)
16
+ useEffect(() => {
17
+ log(x, z);
18
+ }, [x, y]);
19
+
20
+ // error: missing dep x
21
+ useEffect(() => {
22
+ log(x);
23
+ }, [x.y]);
24
+}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-effect-deps.expect.md
+1
-1
@@ -2,7 +2,7 @@
2
## Input
3
4
```javascript
5
-// @validateExhaustiveEffectDependencies
5
+// @validateExhaustiveEffectDependencies:"all"
6
import {useEffect} from 'react';
7
8
function Component({x, y, z}) {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-effect-deps.js
+1
-1
@@ -1,4 +1,4 @@
1
-// @validateExhaustiveEffectDependencies
1
+// @validateExhaustiveEffectDependencies:"all"
2
import {useEffect} from 'react';
3
4
function Component({x, y, z}) {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps-allow-nonreactive-stable-types-as-extra-deps.expect.md
+2
-2
@@ -2,7 +2,7 @@
2
## Input
3
4
```javascript
5
-// @validateExhaustiveMemoizationDependencies @validateExhaustiveEffectDependencies
5
+// @validateExhaustiveMemoizationDependencies @validateExhaustiveEffectDependencies:"all"
6
import {
7
useCallback,
8
useTransition,
@@ -69,7 +69,7 @@ export const FIXTURE_ENTRYPOINT = {
69
## Code
70
71
```javascript
72
-import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveMemoizationDependencies @validateExhaustiveEffectDependencies
72
+import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveMemoizationDependencies @validateExhaustiveEffectDependencies:"all"
73
import {
74
useCallback,
75
useTransition,
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps-allow-nonreactive-stable-types-as-extra-deps.js
+1
-1
@@ -1,4 +1,4 @@
1
-// @validateExhaustiveMemoizationDependencies @validateExhaustiveEffectDependencies
1
+// @validateExhaustiveMemoizationDependencies @validateExhaustiveEffectDependencies:"all"
2
import {
3
useCallback,
4
useTransition,
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps-effect-events.expect.md
+2
-2
@@ -2,7 +2,7 @@
2
## Input
3
4
```javascript
5
-// @validateExhaustiveEffectDependencies
5
+// @validateExhaustiveEffectDependencies:"all"
6
import {useEffect, useEffectEvent} from 'react';
7
8
function Component({x, y, z}) {
@@ -30,7 +30,7 @@ function Component({x, y, z}) {
30
## Code
31
32
```javascript
33
-import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveEffectDependencies
33
+import { c as _c } from "react/compiler-runtime"; // @validateExhaustiveEffectDependencies:"all"
34
import { useEffect, useEffectEvent } from "react";
35
36
function Component(t0) {
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps-effect-events.js
+1
-1
@@ -1,4 +1,4 @@
1
-// @validateExhaustiveEffectDependencies
1
+// @validateExhaustiveEffectDependencies:"all"
2
import {useEffect, useEffectEvent} from 'react';
3
4
function Component({x, y, z}) {
fixtures/eslint-v9/index.js
+13
@@ -167,3 +167,16 @@ function InvalidUseMemo({items}) {
167
const sorted = useMemo(() => [...items].sort(), []);
168
return <div>{sorted.length}</div>;
169
}
170
+
171
+// Invalid: missing/extra deps in useEffect
172
+function InvalidEffectDeps({a, b}) {
173
+ useEffect(() => {
174
+ console.log(a);
175
+ // eslint-disable-next-line react-hooks/exhaustive-deps
176
+ }, []);
177
+
178
+ useEffect(() => {
179
+ console.log(a);
180
+ // TODO: eslint-disable-next-line react-hooks/exhaustive-effect-dependencies
181
+ }, [a, b]);
182
+}
fixtures/eslint-v9/yarn.lock
+17
-5
@@ -603,6 +603,18 @@ has-flag@^4.0.0:
603
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
604
integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
605
606
+hermes-estree@0.25.1:
607
+ version "0.25.1"
608
+ resolved "https://registry.yarnpkg.com/hermes-estree/-/hermes-estree-0.25.1.tgz#6aeec17d1983b4eabf69721f3aa3eb705b17f480"
609
+ integrity sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==
610
+
611
+hermes-parser@^0.25.1:
612
+ version "0.25.1"
613
+ resolved "https://registry.yarnpkg.com/hermes-parser/-/hermes-parser-0.25.1.tgz#5be0e487b2090886c62bd8a11724cd766d5f54d1"
614
+ integrity sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==
615
+ dependencies:
616
+ hermes-estree "0.25.1"
617
+
618
ignore@^5.2.0:
619
version "5.3.2"
620
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5"
@@ -877,12 +889,12 @@ yocto-queue@^0.1.0:
889
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
890
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
891
880
-"zod-validation-error@^3.0.3 || ^4.0.0":
892
+"zod-validation-error@^3.5.0 || ^4.0.0":
893
version "4.0.2"
894
resolved "https://registry.yarnpkg.com/zod-validation-error/-/zod-validation-error-4.0.2.tgz#bc605eba49ce0fcd598c127fee1c236be3f22918"
895
integrity sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==
896
885
-"zod@^3.22.4 || ^4.0.0":
886
- version "4.1.11"
887
- resolved "https://registry.yarnpkg.com/zod/-/zod-4.1.11.tgz#4aab62f76cfd45e6c6166519ba31b2ea019f75f5"
888
- integrity sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==
897
+"zod@^3.25.0 || ^4.0.0":
898
+ version "4.2.0"
899
+ resolved "https://registry.yarnpkg.com/zod/-/zod-4.2.0.tgz#01e86f2c2b6d525a1b9fa6dbe78beccad082118f"
900
+ integrity sha512-Bd5fw9wlIhtqCCxotZgdTOMwGm1a0u75wARVEY9HMs1X17trvA/lMi4+MGK5EUfYkXVTbX8UDiDKW4OgzHVUZw==
packages/eslint-plugin-react-hooks/src/shared/RunReactCompiler.ts
+1
@@ -42,6 +42,7 @@ const COMPILER_OPTIONS: PluginOptions = {
42
// Temporarily enabled for internal testing
43
enableUseKeyedState: true,
44
enableVerboseNoSetStateInEffect: true,
45
+ validateExhaustiveEffectDependencies: 'extra-only',
46
},
47
};
48