@samitouri / QOS-React-1 / commits / 88ee1f5955

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