@samitouri / QOS-React-1 / commits / fb18ad3fd3

[compiler] Exhaustive deps: extra tests, improve diagnostic (#35213)

First, this adds some more tests and organizes them into an `exhaustive-deps/` subdirectory. Second, the diagnostics are overhauled. For each memo block we now report a single diagnostic which summarizes the issue, plus individual errors for each missing/extra dependency. Within the extra deps, we distinguish whether it's truly extra vs whether its just a more (too) precise version of an inferred dep. For example, if you depend on `x.y.z` but the inferred dep was `x.y`. Finally, we print the full inferred deps at the end as a hint (it's also a suggestion, but this makes it more clear what would be suggested).

Joseph Savona committed Nov 25, 2025 at 12:09 UTC fb18ad3fd372623190a8f74387b79e28151cefc4
28 files changed +545 -160
compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts
+1
@@ -807,6 +807,7 @@ export type ManualMemoDependency = {
807 }
808 | {kind: 'Global'; identifierName: string};
809 path: DependencyPath;
810 + loc: SourceLocation;
811 };
812
813 export type StartMemoize = {
compiler/packages/babel-plugin-react-compiler/src/Inference/DropManualMemoization.ts
+3
@@ -65,6 +65,7 @@ export function collectMaybeMemoDependencies(
65 identifierName: value.binding.name,
66 },
67 path: [],
68 + loc: value.loc,
69 };
70 }
71 case 'PropertyLoad': {
@@ -74,6 +75,7 @@ export function collectMaybeMemoDependencies(
75 root: object.root,
76 // TODO: determine if the access is optional
77 path: [...object.path, {property: value.property, optional}],
78 + loc: value.loc,
79 };
80 }
81 break;
@@ -95,6 +97,7 @@ export function collectMaybeMemoDependencies(
97 constant: false,
98 },
99 path: [],
100 + loc: value.place.loc,
101 };
102 }
103 break;
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateExhaustiveDependencies.ts
+105 -43
@@ -241,11 +241,10 @@ export function validateExhaustiveDependencies(
241 matched.add(manualDependency);
242 }
243 }
244 - const isOptionalDependency =
245 - !reactive.has(inferredDependency.identifier.id) &&
246 - (isStableType(inferredDependency.identifier) ||
247 - isPrimitiveType(inferredDependency.identifier));
248 - if (hasMatchingManualDependency || isOptionalDependency) {
244 + if (
245 + hasMatchingManualDependency ||
246 + isOptionalDependency(inferredDependency, reactive)
247 + ) {
248 continue;
249 }
250 missing.push(inferredDependency);
@@ -274,54 +273,106 @@ export function validateExhaustiveDependencies(
273 }
274
275 if (missing.length !== 0 || extra.length !== 0) {
277 - let suggestions: Array<CompilerSuggestion> | null = null;
276 + let suggestion: CompilerSuggestion | null = null;
277 if (startMemo.depsLoc != null && typeof startMemo.depsLoc !== 'symbol') {
279 - suggestions = [
280 - {
281 - description: 'Update dependencies',
282 - range: [startMemo.depsLoc.start.index, startMemo.depsLoc.end.index],
283 - op: CompilerSuggestionOperation.Replace,
284 - text: `[${inferred.map(printInferredDependency).join(', ')}]`,
285 - },
286 - ];
278 + suggestion = {
279 + description: 'Update dependencies',
280 + range: [startMemo.depsLoc.start.index, startMemo.depsLoc.end.index],
281 + op: CompilerSuggestionOperation.Replace,
282 + text: `[${inferred
283 + .filter(
284 + dep =>
285 + dep.kind === 'Local' && !isOptionalDependency(dep, reactive),
286 + )
287 + .map(printInferredDependency)
288 + .join(', ')}]`,
289 + };
290 }
288 - if (missing.length !== 0) {
289 - const diagnostic = CompilerDiagnostic.create({
290 - category: ErrorCategory.MemoDependencies,
291 - reason: 'Found missing memoization dependencies',
292 - description:
293 - 'Missing dependencies can cause a value not to update when those inputs change, ' +
294 - 'resulting in stale UI',
295 - suggestions,
291 + const diagnostic = CompilerDiagnostic.create({
292 + category: ErrorCategory.MemoDependencies,
293 + reason: 'Found missing/extra memoization dependencies',
294 + description: [
295 + missing.length !== 0
296 + ? 'Missing dependencies can cause a value to update less often than it should, ' +
297 + 'resulting in stale UI'
298 + : null,
299 + extra.length !== 0
300 + ? 'Extra dependencies can cause a value to update more often than it should, ' +
301 + 'resulting in performance problems such as excessive renders or effects firing too often'
302 + : null,
303 + ]
304 + .filter(Boolean)
305 + .join('. '),
306 + suggestions: suggestion != null ? [suggestion] : null,
307 + });
308 + for (const dep of missing) {
309 + let reactiveStableValueHint = '';
310 + if (isStableType(dep.identifier)) {
311 + reactiveStableValueHint =
312 + '. Refs, setState functions, and other "stable" values generally do not need to be added ' +
313 + 'as dependencies, but this variable may change over time to point to different values';
314 + }
315 + diagnostic.withDetails({
316 + kind: 'error',
317 + message: `Missing dependency \`${printInferredDependency(dep)}\`${reactiveStableValueHint}`,
318 + loc: dep.loc,
319 });
297 - for (const dep of missing) {
298 - let reactiveStableValueHint = '';
299 - if (isStableType(dep.identifier)) {
300 - reactiveStableValueHint =
301 - '. Refs, setState functions, and other "stable" values generally do not need to be added as dependencies, but this variable may change over time to point to different values';
302 - }
320 + }
321 + for (const dep of extra) {
322 + if (dep.root.kind === 'Global') {
323 diagnostic.withDetails({
324 kind: 'error',
305 - message: `Missing dependency \`${printInferredDependency(dep)}\`${reactiveStableValueHint}`,
306 - loc: dep.loc,
325 + message:
326 + `Unnecessary dependency \`${printManualMemoDependency(dep)}\`. ` +
327 + 'Values declared outside of a component/hook should not be listed as ' +
328 + 'dependencies as the component will not re-render if they change',
329 + loc: dep.loc ?? startMemo.depsLoc ?? value.loc,
330 });
331 + error.pushDiagnostic(diagnostic);
332 + } else {
333 + const root = dep.root.value;
334 + const matchingInferred = inferred.find(
335 + (
336 + inferredDep,
337 + ): inferredDep is Extract<InferredDependency, {kind: 'Local'}> => {
338 + return (
339 + inferredDep.kind === 'Local' &&
340 + inferredDep.identifier.id === root.identifier.id &&
341 + isSubPathIgnoringOptionals(inferredDep.path, dep.path)
342 + );
343 + },
344 + );
345 + if (
346 + matchingInferred != null &&
347 + !isOptionalDependency(matchingInferred, reactive)
348 + ) {
349 + diagnostic.withDetails({
350 + kind: 'error',
351 + message:
352 + `Overly precise dependency \`${printManualMemoDependency(dep)}\`, ` +
353 + `use \`${printInferredDependency(matchingInferred)}\` instead`,
354 + loc: dep.loc ?? startMemo.depsLoc ?? value.loc,
355 + });
356 + } else {
357 + /**
358 + * Else this dependency doesn't correspond to anything referenced in the memo function,
359 + * or is an optional dependency so we don't want to suggest adding it
360 + */
361 + diagnostic.withDetails({
362 + kind: 'error',
363 + message: `Unnecessary dependency \`${printManualMemoDependency(dep)}\``,
364 + loc: dep.loc ?? startMemo.depsLoc ?? value.loc,
365 + });
366 + }
367 }
309 - error.pushDiagnostic(diagnostic);
310 - } else if (extra.length !== 0) {
311 - const diagnostic = CompilerDiagnostic.create({
312 - category: ErrorCategory.MemoDependencies,
313 - reason: 'Found unnecessary memoization dependencies',
314 - description:
315 - 'Unnecessary dependencies can cause a value to update more often than necessary, ' +
316 - 'causing performance regressions and effects to fire more often than expected',
317 - });
368 + }
369 + if (suggestion != null) {
370 diagnostic.withDetails({
319 - kind: 'error',
320 - message: `Unnecessary dependencies ${extra.map(dep => `\`${printManualMemoDependency(dep)}\``).join(', ')}`,
321 - loc: startMemo.depsLoc ?? value.loc,
371 + kind: 'hint',
372 + message: `Inferred dependencies: \`${suggestion.text}\``,
373 });
323 - error.pushDiagnostic(diagnostic);
374 }
375 + error.pushDiagnostic(diagnostic);
376 }
377
378 dependencies.clear();
@@ -826,3 +877,14 @@ export function findOptionalPlaces(
877 }
878 return optionals;
879 }
880 +
881 +function isOptionalDependency(
882 + inferredDependency: Extract<InferredDependency, {kind: 'Local'}>,
883 + reactive: Set<IdentifierId>,
884 +): boolean {
885 + return (
886 + !reactive.has(inferredDependency.identifier.id) &&
887 + (isStableType(inferredDependency.identifier) ||
888 + isPrimitiveType(inferredDependency.identifier))
889 + );
890 +}
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts
+4
@@ -242,6 +242,7 @@ function validateInferredDep(
242 normalizedDep = {
243 root: maybeNormalizedRoot.root,
244 path: [...maybeNormalizedRoot.path, ...dep.path],
245 + loc: maybeNormalizedRoot.loc,
246 };
247 } else {
248 CompilerError.invariant(dep.identifier.name?.kind === 'named', {
@@ -270,6 +271,7 @@ function validateInferredDep(
271 constant: false,
272 },
273 path: [...dep.path],
274 + loc: GeneratedSource,
275 };
276 }
277 for (const decl of declsWithinMemoBlock) {
@@ -383,6 +385,7 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
385 constant: false,
386 },
387 path: [],
388 + loc: storeTarget.loc,
389 });
390 }
391 }
@@ -413,6 +416,7 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
416 constant: false,
417 },
418 path: [],
419 + loc: lvalue.loc,
420 });
421 }
422 }
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-exhaustive-deps.expect.md deleted
-109
@@ -1,109 +0,0 @@
1 -
2 -## Input
3 -
4 -```javascript
5 -// @validateExhaustiveMemoizationDependencies
6 -import {useMemo} from 'react';
7 -import {Stringify} from 'shared-runtime';
8 -
9 -function Component({x, y, z}) {
10 - const a = useMemo(() => {
11 - return x?.y.z?.a;
12 - // error: too precise
13 - }, [x?.y.z?.a.b]);
14 - const b = useMemo(() => {
15 - return x.y.z?.a;
16 - // ok, not our job to type check nullability
17 - }, [x.y.z.a]);
18 - const c = useMemo(() => {
19 - return x?.y.z.a?.b;
20 - // error: too precise
21 - }, [x?.y.z.a?.b.z]);
22 - const d = useMemo(() => {
23 - return x?.y?.[(console.log(y), z?.b)];
24 - // ok
25 - }, [x?.y, y, z?.b]);
26 - const e = useMemo(() => {
27 - const e = [];
28 - e.push(x);
29 - return e;
30 - // ok
31 - }, [x]);
32 - const f = useMemo(() => {
33 - return [];
34 - // error: unnecessary
35 - }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
36 - const ref1 = useRef(null);
37 - const ref2 = useRef(null);
38 - const ref = z ? ref1 : ref2;
39 - const cb = useMemo(() => {
40 - return () => {
41 - return ref.current;
42 - };
43 - // error: ref is a stable type but reactive
44 - }, []);
45 - return <Stringify results={[a, b, c, d, e, f, cb]} />;
46 -}
47 -
48 -```
49 -
50 -
51 -## Error
52 -
53 -```
54 -Found 4 errors:
55 -
56 -Error: Found missing memoization dependencies
57 -
58 -Missing dependencies can cause a value not to update when those inputs change, resulting in stale UI.
59 -
60 -error.invalid-exhaustive-deps.ts:7:11
61 - 5 | function Component({x, y, z}) {
62 - 6 | const a = useMemo(() => {
63 -> 7 | return x?.y.z?.a;
64 - | ^^^^^^^^^ Missing dependency `x?.y.z?.a`
65 - 8 | // error: too precise
66 - 9 | }, [x?.y.z?.a.b]);
67 - 10 | const b = useMemo(() => {
68 -
69 -Error: Found missing memoization dependencies
70 -
71 -Missing dependencies can cause a value not to update when those inputs change, resulting in stale UI.
72 -
73 -error.invalid-exhaustive-deps.ts:15:11
74 - 13 | }, [x.y.z.a]);
75 - 14 | const c = useMemo(() => {
76 -> 15 | return x?.y.z.a?.b;
77 - | ^^^^^^^^^^^ Missing dependency `x?.y.z.a?.b`
78 - 16 | // error: too precise
79 - 17 | }, [x?.y.z.a?.b.z]);
80 - 18 | const d = useMemo(() => {
81 -
82 -Error: Found unnecessary memoization dependencies
83 -
84 -Unnecessary dependencies can cause a value to update more often than necessary, causing performance regressions and effects to fire more often than expected.
85 -
86 -error.invalid-exhaustive-deps.ts:31:5
87 - 29 | return [];
88 - 30 | // error: unnecessary
89 -> 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
90 - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Unnecessary dependencies `x`, `y.z`, `z?.y?.a`, `UNUSED_GLOBAL`
91 - 32 | const ref1 = useRef(null);
92 - 33 | const ref2 = useRef(null);
93 - 34 | const ref = z ? ref1 : ref2;
94 -
95 -Error: Found missing memoization dependencies
96 -
97 -Missing dependencies can cause a value not to update when those inputs change, resulting in stale UI.
98 -
99 -error.invalid-exhaustive-deps.ts:37:13
100 - 35 | const cb = useMemo(() => {
101 - 36 | return () => {
102 -> 37 | return ref.current;
103 - | ^^^ Missing dependency `ref`. Refs, setState functions, and other "stable" values generally do not need to be added as dependencies, but this variable may change over time to point to different values
104 - 38 | };
105 - 39 | // error: ref is a stable type but reactive
106 - 40 | }, []);
107 -```
108 -
109 -
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/compile-files-with-exhaustive-deps-violation-in-effects.expect.md renamed
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/compile-files-with-exhaustive-deps-violation-in-effects.js renamed
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-dep-on-ref-current-value.expect.md new
+40
@@ -0,0 +1,40 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateExhaustiveMemoizationDependencies
6 +
7 +function Component() {
8 + const ref = useRef(null);
9 + const onChange = useCallback(() => {
10 + return ref.current.value;
11 + }, [ref.current.value]);
12 +
13 + return <input ref={ref} onChange={onChange} />;
14 +}
15 +
16 +```
17 +
18 +
19 +## Error
20 +
21 +```
22 +Found 1 error:
23 +
24 +Error: Found missing/extra memoization dependencies
25 +
26 +Extra dependencies can cause a value to update more often than it should, resulting in performance problems such as excessive renders or effects firing too often.
27 +
28 +error.invalid-dep-on-ref-current-value.ts:7:6
29 + 5 | const onChange = useCallback(() => {
30 + 6 | return ref.current.value;
31 +> 7 | }, [ref.current.value]);
32 + | ^^^^^^^^^^^^^^^^^ Unnecessary dependency `ref.current.value`
33 + 8 |
34 + 9 | return <input ref={ref} onChange={onChange} />;
35 + 10 | }
36 +
37 +Inferred dependencies: `[]`
38 +```
39 +
40 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-dep-on-ref-current-value.js new
+10
@@ -0,0 +1,10 @@
1 +// @validateExhaustiveMemoizationDependencies
2 +
3 +function Component() {
4 + const ref = useRef(null);
5 + const onChange = useCallback(() => {
6 + return ref.current.value;
7 + }, [ref.current.value]);
8 +
9 + return <input ref={ref} onChange={onChange} />;
10 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-deps-disallow-unused-stable-types.expect.md renamed
+6 -4
@@ -25,18 +25,20 @@ function Component() {
25 ```
26 Found 1 error:
27
28 -Error: Found unnecessary memoization dependencies
28 +Error: Found missing/extra memoization dependencies
29
30 -Unnecessary dependencies can cause a value to update more often than necessary, causing performance regressions and effects to fire more often than expected.
30 +Extra dependencies can cause a value to update more often than it should, resulting in performance problems such as excessive renders or effects firing too often.
31
32 -error.invalid-exhaustive-deps-disallow-unused-stable-types.ts:11:5
32 +error.invalid-exhaustive-deps-disallow-unused-stable-types.ts:11:13
33 9 | return [state];
34 10 | // error: `setState` is a stable type, but not actually referenced
35 > 11 | }, [state, setState]);
36 - | ^^^^^^^^^^^^^^^^^ Unnecessary dependencies `setState`
36 + | ^^^^^^^^ Unnecessary dependency `setState`
37 12 |
38 13 | return 'oops';
39 14 | }
40 +
41 +Inferred dependencies: `[state]`
42 ```
43
44
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-deps-disallow-unused-stable-types.js renamed
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-deps.expect.md new
+204
@@ -0,0 +1,204 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateExhaustiveMemoizationDependencies
6 +import {useMemo} from 'react';
7 +import {Stringify} from 'shared-runtime';
8 +
9 +function Component({x, y, z}) {
10 + const a = useMemo(() => {
11 + return x?.y.z?.a;
12 + // error: too precise
13 + }, [x?.y.z?.a.b]);
14 + const b = useMemo(() => {
15 + return x.y.z?.a;
16 + // ok, not our job to type check nullability
17 + }, [x.y.z.a]);
18 + const c = useMemo(() => {
19 + return x?.y.z.a?.b;
20 + // error: too precise
21 + }, [x?.y.z.a?.b.z]);
22 + const d = useMemo(() => {
23 + return x?.y?.[(console.log(y), z?.b)];
24 + // ok
25 + }, [x?.y, y, z?.b]);
26 + const e = useMemo(() => {
27 + const e = [];
28 + e.push(x);
29 + return e;
30 + // ok
31 + }, [x]);
32 + const f = useMemo(() => {
33 + return [];
34 + // error: unnecessary
35 + }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
36 + const ref1 = useRef(null);
37 + const ref2 = useRef(null);
38 + const ref = z ? ref1 : ref2;
39 + const cb = useMemo(() => {
40 + return () => {
41 + return ref.current;
42 + };
43 + // error: ref is a stable type but reactive
44 + }, []);
45 + return <Stringify results={[a, b, c, d, e, f, cb]} />;
46 +}
47 +
48 +```
49 +
50 +
51 +## Error
52 +
53 +```
54 +Found 5 errors:
55 +
56 +Error: Found missing/extra memoization dependencies
57 +
58 +Missing dependencies can cause a value to update less often than it should, resulting in stale UI. Extra dependencies can cause a value to update more often than it should, resulting in performance problems such as excessive renders or effects firing too often.
59 +
60 +error.invalid-exhaustive-deps.ts:7:11
61 + 5 | function Component({x, y, z}) {
62 + 6 | const a = useMemo(() => {
63 +> 7 | return x?.y.z?.a;
64 + | ^^^^^^^^^ Missing dependency `x?.y.z?.a`
65 + 8 | // error: too precise
66 + 9 | }, [x?.y.z?.a.b]);
67 + 10 | const b = useMemo(() => {
68 +
69 +error.invalid-exhaustive-deps.ts:9:6
70 + 7 | return x?.y.z?.a;
71 + 8 | // error: too precise
72 +> 9 | }, [x?.y.z?.a.b]);
73 + | ^^^^^^^^^^^ Overly precise dependency `x?.y.z?.a.b`, use `x?.y.z?.a` instead
74 + 10 | const b = useMemo(() => {
75 + 11 | return x.y.z?.a;
76 + 12 | // ok, not our job to type check nullability
77 +
78 +Inferred dependencies: `[x?.y.z?.a]`
79 +
80 +Error: Found missing/extra memoization dependencies
81 +
82 +Missing dependencies can cause a value to update less often than it should, resulting in stale UI. Extra dependencies can cause a value to update more often than it should, resulting in performance problems such as excessive renders or effects firing too often.
83 +
84 +error.invalid-exhaustive-deps.ts:15:11
85 + 13 | }, [x.y.z.a]);
86 + 14 | const c = useMemo(() => {
87 +> 15 | return x?.y.z.a?.b;
88 + | ^^^^^^^^^^^ Missing dependency `x?.y.z.a?.b`
89 + 16 | // error: too precise
90 + 17 | }, [x?.y.z.a?.b.z]);
91 + 18 | const d = useMemo(() => {
92 +
93 +error.invalid-exhaustive-deps.ts:17:6
94 + 15 | return x?.y.z.a?.b;
95 + 16 | // error: too precise
96 +> 17 | }, [x?.y.z.a?.b.z]);
97 + | ^^^^^^^^^^^^^ Overly precise dependency `x?.y.z.a?.b.z`, use `x?.y.z.a?.b` instead
98 + 18 | const d = useMemo(() => {
99 + 19 | return x?.y?.[(console.log(y), z?.b)];
100 + 20 | // ok
101 +
102 +Inferred dependencies: `[x?.y.z.a?.b]`
103 +
104 +Error: Found missing/extra memoization dependencies
105 +
106 +Extra dependencies can cause a value to update more often than it should, resulting in performance problems such as excessive renders or effects firing too often.
107 +
108 +error.invalid-exhaustive-deps.ts:31:6
109 + 29 | return [];
110 + 30 | // error: unnecessary
111 +> 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
112 + | ^ Unnecessary dependency `x`
113 + 32 | const ref1 = useRef(null);
114 + 33 | const ref2 = useRef(null);
115 + 34 | const ref = z ? ref1 : ref2;
116 +
117 +error.invalid-exhaustive-deps.ts:31:9
118 + 29 | return [];
119 + 30 | // error: unnecessary
120 +> 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
121 + | ^^^ Unnecessary dependency `y.z`
122 + 32 | const ref1 = useRef(null);
123 + 33 | const ref2 = useRef(null);
124 + 34 | const ref = z ? ref1 : ref2;
125 +
126 +error.invalid-exhaustive-deps.ts:31:14
127 + 29 | return [];
128 + 30 | // error: unnecessary
129 +> 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
130 + | ^^^^^^^ Unnecessary dependency `z?.y?.a`
131 + 32 | const ref1 = useRef(null);
132 + 33 | const ref2 = useRef(null);
133 + 34 | const ref = z ? ref1 : ref2;
134 +
135 +error.invalid-exhaustive-deps.ts:31:23
136 + 29 | return [];
137 + 30 | // error: unnecessary
138 +> 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
139 + | ^^^^^^^^^^^^^ Unnecessary dependency `UNUSED_GLOBAL`. Values declared outside of a component/hook should not be listed as dependencies as the component will not re-render if they change
140 + 32 | const ref1 = useRef(null);
141 + 33 | const ref2 = useRef(null);
142 + 34 | const ref = z ? ref1 : ref2;
143 +
144 +Inferred dependencies: `[]`
145 +
146 +Error: Found missing/extra memoization dependencies
147 +
148 +Extra dependencies can cause a value to update more often than it should, resulting in performance problems such as excessive renders or effects firing too often.
149 +
150 +error.invalid-exhaustive-deps.ts:31:6
151 + 29 | return [];
152 + 30 | // error: unnecessary
153 +> 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
154 + | ^ Unnecessary dependency `x`
155 + 32 | const ref1 = useRef(null);
156 + 33 | const ref2 = useRef(null);
157 + 34 | const ref = z ? ref1 : ref2;
158 +
159 +error.invalid-exhaustive-deps.ts:31:9
160 + 29 | return [];
161 + 30 | // error: unnecessary
162 +> 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
163 + | ^^^ Unnecessary dependency `y.z`
164 + 32 | const ref1 = useRef(null);
165 + 33 | const ref2 = useRef(null);
166 + 34 | const ref = z ? ref1 : ref2;
167 +
168 +error.invalid-exhaustive-deps.ts:31:14
169 + 29 | return [];
170 + 30 | // error: unnecessary
171 +> 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
172 + | ^^^^^^^ Unnecessary dependency `z?.y?.a`
173 + 32 | const ref1 = useRef(null);
174 + 33 | const ref2 = useRef(null);
175 + 34 | const ref = z ? ref1 : ref2;
176 +
177 +error.invalid-exhaustive-deps.ts:31:23
178 + 29 | return [];
179 + 30 | // error: unnecessary
180 +> 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
181 + | ^^^^^^^^^^^^^ Unnecessary dependency `UNUSED_GLOBAL`. Values declared outside of a component/hook should not be listed as dependencies as the component will not re-render if they change
182 + 32 | const ref1 = useRef(null);
183 + 33 | const ref2 = useRef(null);
184 + 34 | const ref = z ? ref1 : ref2;
185 +
186 +Inferred dependencies: `[]`
187 +
188 +Error: Found missing/extra memoization dependencies
189 +
190 +Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
191 +
192 +error.invalid-exhaustive-deps.ts:37:13
193 + 35 | const cb = useMemo(() => {
194 + 36 | return () => {
195 +> 37 | return ref.current;
196 + | ^^^ Missing dependency `ref`. Refs, setState functions, and other "stable" values generally do not need to be added as dependencies, but this variable may change over time to point to different values
197 + 38 | };
198 + 39 | // error: ref is a stable type but reactive
199 + 40 | }, []);
200 +
201 +Inferred dependencies: `[ref]`
202 +```
203 +
204 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-deps.js renamed
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-missing-nonreactive-dep-inner-function.expect.md new
+43
@@ -0,0 +1,43 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateExhaustiveMemoizationDependencies
6 +
7 +import {useMemo} from 'react';
8 +import {makeObject_Primitives} from 'shared-runtime';
9 +
10 +function useHook() {
11 + const object = makeObject_Primitives();
12 + const fn = useCallback(() => {
13 + const g = () => {
14 + return [object];
15 + };
16 + return g;
17 + });
18 + return fn;
19 +}
20 +
21 +```
22 +
23 +
24 +## Error
25 +
26 +```
27 +Found 1 error:
28 +
29 +Error: Found missing/extra memoization dependencies
30 +
31 +Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
32 +
33 +error.invalid-missing-nonreactive-dep-inner-function.ts:10:14
34 + 8 | const fn = useCallback(() => {
35 + 9 | const g = () => {
36 +> 10 | return [object];
37 + | ^^^^^^ Missing dependency `object`
38 + 11 | };
39 + 12 | return g;
40 + 13 | });
41 +```
42 +
43 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-missing-nonreactive-dep-inner-function.js new
+15
@@ -0,0 +1,15 @@
1 +// @validateExhaustiveMemoizationDependencies
2 +
3 +import {useMemo} from 'react';
4 +import {makeObject_Primitives} from 'shared-runtime';
5 +
6 +function useHook() {
7 + const object = makeObject_Primitives();
8 + const fn = useCallback(() => {
9 + const g = () => {
10 + return [object];
11 + };
12 + return g;
13 + });
14 + return fn;
15 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-missing-nonreactive-dep-unmemoized.expect.md new
+43
@@ -0,0 +1,43 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateExhaustiveMemoizationDependencies
6 +
7 +import {useMemo} from 'react';
8 +import {makeObject_Primitives, useIdentity} from 'shared-runtime';
9 +
10 +function useHook() {
11 + // object is non-reactive but not memoized bc the mutation surrounds a hook
12 + const object = makeObject_Primitives();
13 + useIdentity();
14 + object.x = 0;
15 + const array = useMemo(() => [object], []);
16 + return array;
17 +}
18 +
19 +```
20 +
21 +
22 +## Error
23 +
24 +```
25 +Found 1 error:
26 +
27 +Error: Found missing/extra memoization dependencies
28 +
29 +Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
30 +
31 +error.invalid-missing-nonreactive-dep-unmemoized.ts:11:31
32 + 9 | useIdentity();
33 + 10 | object.x = 0;
34 +> 11 | const array = useMemo(() => [object], []);
35 + | ^^^^^^ Missing dependency `object`
36 + 12 | return array;
37 + 13 | }
38 + 14 |
39 +
40 +Inferred dependencies: `[object]`
41 +```
42 +
43 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-missing-nonreactive-dep-unmemoized.js new
+13
@@ -0,0 +1,13 @@
1 +// @validateExhaustiveMemoizationDependencies
2 +
3 +import {useMemo} from 'react';
4 +import {makeObject_Primitives, useIdentity} from 'shared-runtime';
5 +
6 +function useHook() {
7 + // object is non-reactive but not memoized bc the mutation surrounds a hook
8 + const object = makeObject_Primitives();
9 + useIdentity();
10 + object.x = 0;
11 + const array = useMemo(() => [object], []);
12 + return array;
13 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-missing-nonreactive-dep.expect.md new
+40
@@ -0,0 +1,40 @@
1 +
2 +## Input
3 +
4 +```javascript
5 +// @validateExhaustiveMemoizationDependencies
6 +
7 +import {useMemo} from 'react';
8 +import {makeObject_Primitives} from 'shared-runtime';
9 +
10 +function useHook() {
11 + const object = makeObject_Primitives();
12 + const array = useMemo(() => [object], []);
13 + return array;
14 +}
15 +
16 +```
17 +
18 +
19 +## Error
20 +
21 +```
22 +Found 1 error:
23 +
24 +Error: Found missing/extra memoization dependencies
25 +
26 +Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
27 +
28 +error.invalid-missing-nonreactive-dep.ts:8:31
29 + 6 | function useHook() {
30 + 7 | const object = makeObject_Primitives();
31 +> 8 | const array = useMemo(() => [object], []);
32 + | ^^^^^^ Missing dependency `object`
33 + 9 | return array;
34 + 10 | }
35 + 11 |
36 +
37 +Inferred dependencies: `[object]`
38 +```
39 +
40 +
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-missing-nonreactive-dep.js new
+10
@@ -0,0 +1,10 @@
1 +// @validateExhaustiveMemoizationDependencies
2 +
3 +import {useMemo} from 'react';
4 +import {makeObject_Primitives} from 'shared-runtime';
5 +
6 +function useHook() {
7 + const object = makeObject_Primitives();
8 + const array = useMemo(() => [object], []);
9 + return array;
10 +}
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.sketchy-code-exhaustive-deps.expect.md renamed
+4 -2
@@ -25,9 +25,9 @@ function Component() {
25 ```
26 Found 1 error:
27
28 -Error: Found missing memoization dependencies
28 +Error: Found missing/extra memoization dependencies
29
30 -Missing dependencies can cause a value not to update when those inputs change, resulting in stale UI.
30 +Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
31
32 error.sketchy-code-exhaustive-deps.ts:8:16
33 6 | const foo = useCallback(
@@ -37,6 +37,8 @@ error.sketchy-code-exhaustive-deps.ts:8:16
37 9 | }, // eslint-disable-next-line react-hooks/exhaustive-deps
38 10 | []
39 11 | );
40 +
41 +Inferred dependencies: `[item]`
42 ```
43
44
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.sketchy-code-exhaustive-deps.js renamed
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps-allow-constant-folded-values.expect.md renamed
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps-allow-constant-folded-values.js renamed
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps-allow-nonreactive-stable-types-as-extra-deps.expect.md renamed
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps-allow-nonreactive-stable-types-as-extra-deps.js renamed
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps.expect.md renamed
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/exhaustive-deps.js renamed
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-unrelated-mutation-in-depslist.expect.md
+4 -2
@@ -32,9 +32,9 @@ function useFoo(input1) {
32 ```
33 Found 1 error:
34
35 -Error: Found missing memoization dependencies
35 +Error: Found missing/extra memoization dependencies
36
37 -Missing dependencies can cause a value not to update when those inputs change, resulting in stale UI.
37 +Missing dependencies can cause a value to update less often than it should, resulting in stale UI.
38
39 error.useMemo-unrelated-mutation-in-depslist.ts:18:14
40 16 | const memoized = useMemo(() => {
@@ -44,6 +44,8 @@ error.useMemo-unrelated-mutation-in-depslist.ts:18:14
44 19 |
45 20 | return [x, memoized];
46 21 | }
47 +
48 +Inferred dependencies: `[x, y]`
49 ```
50
51
\ No newline at end of file