@samitouri / QOS-React-2 / commits / 6b113b7bd1

[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