[compiler] Deduplicate errors between ValidateExhaustiveDependencies and ValidatePreservedManualMemoization (#35917)
With the recent changes to make the compiler fault tolerant and always continue through all passes, we can now sometimes report duplicative errors. Specifically, when `ValidateExhaustiveDependencies` finds incorrect deps for a useMemo/useCallback call, `ValidatePreservedManualMemoization` will generally also error for the same block, producing duplicate errors. The exhaustive deps error is strictly more informative, so if we've already reported the earlier error we don't need the later one. This adds a `hasInvalidDeps` flag to StartMemoize that is set when ValidateExhaustiveDependencies produces a diagnostic. ValidatePreservedManualMemoization then skips validation for memo blocks with this flag set.
Joseph Savona committed
Feb 26, 2026 at 12:40 UTC
6b113b7bd11f7fa432a0a8f9375013d30c884494
9 files changed
+27
-129
compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts
+1
@@ -826,6 +826,7 @@ export type StartMemoize = {
826
* emitting diagnostics with a suggested replacement
827
*/
828
depsLoc: SourceLocation | null;
829
+ hasInvalidDeps?: true;
830
loc: SourceLocation;
831
};
832
export type FinishMemoize = {
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateExhaustiveDependencies.ts
+1
@@ -143,6 +143,7 @@ export function validateExhaustiveDependencies(fn: HIRFunction): void {
143
);
144
if (diagnostic != null) {
145
fn.env.recordError(diagnostic);
146
+ startMemo.hasInvalidDeps = true;
147
}
148
}
149
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidatePreservedManualMemoization.ts
+19
-7
@@ -486,16 +486,25 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
486
ids.add(value.place.identifier);
487
}
488
if (value.kind === 'StartMemoize') {
489
- let depsFromSource: Array<ManualMemoDependency> | null = null;
490
- if (value.deps != null) {
491
- depsFromSource = value.deps;
492
- }
489
CompilerError.invariant(state.manualMemoState == null, {
490
reason: 'Unexpected nested StartMemoize instructions',
491
description: `Bad manual memoization ids: ${state.manualMemoState?.manualMemoId}, ${value.manualMemoId}`,
492
loc: value.loc,
493
});
494
495
+ if (value.hasInvalidDeps === true) {
496
+ /*
497
+ * ValidateExhaustiveDependencies already reported an error for this
498
+ * memo block, skip validation to avoid duplicate errors
499
+ */
500
+ return;
501
+ }
502
+
503
+ let depsFromSource: Array<ManualMemoDependency> | null = null;
504
+ if (value.deps != null) {
505
+ depsFromSource = value.deps;
506
+ }
507
+
508
state.manualMemoState = {
509
loc: instruction.loc,
510
decls: new Set(),
@@ -547,12 +556,15 @@ class Visitor extends ReactiveFunctionVisitor<VisitorState> {
556
}
557
}
558
if (value.kind === 'FinishMemoize') {
559
+ if (state.manualMemoState == null) {
560
+ // StartMemoize had invalid deps, skip validation
561
+ return;
562
+ }
563
CompilerError.invariant(
551
- state.manualMemoState != null &&
552
- state.manualMemoState.manualMemoId === value.manualMemoId,
564
+ state.manualMemoState.manualMemoId === value.manualMemoId,
565
{
566
reason: 'Unexpected mismatch between StartMemoize and FinishMemoize',
555
- description: `Encountered StartMemoize id=${state.manualMemoState?.manualMemoId} followed by FinishMemoize id=${value.manualMemoId}`,
567
+ description: `Encountered StartMemoize id=${state.manualMemoState.manualMemoId} followed by FinishMemoize id=${value.manualMemoId}`,
568
loc: value.loc,
569
},
570
);
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-ReactUseMemo-async-callback.expect.md
+1
-17
@@ -15,7 +15,7 @@ function component(a, b) {
15
## Error
16
17
```
18
-Found 3 errors:
18
+Found 2 errors:
19
20
Error: useMemo() callbacks may not be async or generator functions
21
@@ -47,22 +47,6 @@ error.invalid-ReactUseMemo-async-callback.ts:3:10
47
6 | }
48
49
Inferred dependencies: `[a]`
50
-
51
-Compilation Skipped: Existing memoization could not be preserved
52
-
53
-React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `a`, but the source dependencies were []. Inferred dependency not present in source.
54
-
55
-error.invalid-ReactUseMemo-async-callback.ts:2:24
56
- 1 | function component(a, b) {
57
-> 2 | let x = React.useMemo(async () => {
58
- | ^^^^^^^^^^^^^
59
-> 3 | await a;
60
- | ^^^^^^^^^^^^
61
-> 4 | }, []);
62
- | ^^^^ Could not preserve existing manual memoization
63
- 5 | return x;
64
- 6 | }
65
- 7 |
50
```
51
52
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-useMemo-async-callback.expect.md
+1
-17
@@ -15,7 +15,7 @@ function component(a, b) {
15
## Error
16
17
```
18
-Found 3 errors:
18
+Found 2 errors:
19
20
Error: useMemo() callbacks may not be async or generator functions
21
@@ -47,22 +47,6 @@ error.invalid-useMemo-async-callback.ts:3:10
47
6 | }
48
49
Inferred dependencies: `[a]`
50
-
51
-Compilation Skipped: Existing memoization could not be preserved
52
-
53
-React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `a`, but the source dependencies were []. Inferred dependency not present in source.
54
-
55
-error.invalid-useMemo-async-callback.ts:2:18
56
- 1 | function component(a, b) {
57
-> 2 | let x = useMemo(async () => {
58
- | ^^^^^^^^^^^^^
59
-> 3 | await a;
60
- | ^^^^^^^^^^^^
61
-> 4 | }, []);
62
- | ^^^^ Could not preserve existing manual memoization
63
- 5 | return x;
64
- 6 | }
65
- 7 |
50
```
51
52
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-useMemo-callback-args.expect.md
+1
-13
@@ -13,7 +13,7 @@ function component(a, b) {
13
## Error
14
15
```
16
-Found 3 errors:
16
+Found 2 errors:
17
18
Error: useMemo() callbacks may not accept parameters
19
@@ -40,18 +40,6 @@ error.invalid-useMemo-callback-args.ts:2:23
40
5 |
41
42
Inferred dependencies: `[a]`
43
-
44
-Compilation Skipped: Existing memoization could not be preserved
45
-
46
-React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `a`, but the source dependencies were []. Inferred dependency not present in source.
47
-
48
-error.invalid-useMemo-callback-args.ts:2:18
49
- 1 | function component(a, b) {
50
-> 2 | let x = useMemo(c => a, []);
51
- | ^^^^^^ Could not preserve existing manual memoization
52
- 3 | return x;
53
- 4 | }
54
- 5 |
43
```
44
45
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-exhaustive-deps.expect.md
+1
-43
@@ -51,7 +51,7 @@ function Component({x, y, z}) {
51
## Error
52
53
```
54
-Found 6 errors:
54
+Found 4 errors:
55
56
Error: Found missing/extra memoization dependencies
57
@@ -157,48 +157,6 @@ error.invalid-exhaustive-deps.ts:37:13
157
40 | }, []);
158
159
Inferred dependencies: `[ref]`
160
-
161
-Compilation Skipped: Existing memoization could not be preserved
162
-
163
-React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `x.y.z.a.b`, but the source dependencies were [x?.y.z.a?.b.z]. Inferred different dependency than source.
164
-
165
-error.invalid-exhaustive-deps.ts:14:20
166
- 12 | // ok, not our job to type check nullability
167
- 13 | }, [x.y.z.a]);
168
-> 14 | const c = useMemo(() => {
169
- | ^^^^^^^
170
-> 15 | return x?.y.z.a?.b;
171
- | ^^^^^^^^^^^^^^^^^^^^^^^
172
-> 16 | // error: too precise
173
- | ^^^^^^^^^^^^^^^^^^^^^^^
174
-> 17 | }, [x?.y.z.a?.b.z]);
175
- | ^^^^ Could not preserve existing manual memoization
176
- 18 | const d = useMemo(() => {
177
- 19 | return x?.y?.[(console.log(y), z?.b)];
178
- 20 | // ok
179
-
180
-Compilation Skipped: Existing memoization could not be preserved
181
-
182
-React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `ref`, but the source dependencies were []. Inferred dependency not present in source.
183
-
184
-error.invalid-exhaustive-deps.ts:35:21
185
- 33 | const ref2 = useRef(null);
186
- 34 | const ref = z ? ref1 : ref2;
187
-> 35 | const cb = useMemo(() => {
188
- | ^^^^^^^
189
-> 36 | return () => {
190
- | ^^^^^^^^^^^^^^^^^^
191
-> 37 | return ref.current;
192
- | ^^^^^^^^^^^^^^^^^^
193
-> 38 | };
194
- | ^^^^^^^^^^^^^^^^^^
195
-> 39 | // error: ref is a stable type but reactive
196
- | ^^^^^^^^^^^^^^^^^^
197
-> 40 | }, []);
198
- | ^^^^ Could not preserve existing manual memoization
199
- 41 | return <Stringify results={[a, b, c, d, e, f, cb]} />;
200
- 42 | }
201
- 43 |
160
```
161
162
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/exhaustive-deps/error.invalid-missing-nonreactive-dep-unmemoized.expect.md
+1
-14
@@ -22,7 +22,7 @@ function useHook() {
22
## Error
23
24
```
25
-Found 2 errors:
25
+Found 1 error:
26
27
Error: Found missing memoization dependencies
28
@@ -38,19 +38,6 @@ error.invalid-missing-nonreactive-dep-unmemoized.ts:11:31
38
14 |
39
40
Inferred dependencies: `[object]`
41
-
42
-Compilation Skipped: Existing memoization could not be preserved
43
-
44
-React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `object`, but the source dependencies were []. Inferred dependency not present in source.
45
-
46
-error.invalid-missing-nonreactive-dep-unmemoized.ts:11:24
47
- 9 | useIdentity();
48
- 10 | object.x = 0;
49
-> 11 | const array = useMemo(() => [object], []);
50
- | ^^^^^^^^^^^^^^ Could not preserve existing manual memoization
51
- 12 | return array;
52
- 13 | }
53
- 14 |
41
```
42
43
\ No newline at end of file
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.useMemo-unrelated-mutation-in-depslist.expect.md
+1
-18
@@ -30,7 +30,7 @@ function useFoo(input1) {
30
## Error
31
32
```
33
-Found 2 errors:
33
+Found 1 error:
34
35
Error: Found missing memoization dependencies
36
@@ -46,23 +46,6 @@ error.useMemo-unrelated-mutation-in-depslist.ts:18:14
46
21 | }
47
48
Inferred dependencies: `[x, y]`
49
-
50
-Compilation Skipped: Existing memoization could not be preserved
51
-
52
-React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `input1`, but the source dependencies were [y]. Inferred different dependency than source.
53
-
54
-error.useMemo-unrelated-mutation-in-depslist.ts:16:27
55
- 14 | const x = {};
56
- 15 | const y = [input1];
57
-> 16 | const memoized = useMemo(() => {
58
- | ^^^^^^^
59
-> 17 | return [y];
60
- | ^^^^^^^^^^^^^^^
61
-> 18 | }, [(mutate(x), y)]);
62
- | ^^^^ Could not preserve existing manual memoization
63
- 19 |
64
- 20 | return [x, memoized];
65
- 21 | }
49
```
50
51
\ No newline at end of file