@samitouri / QOS-React-1 / commits / 454e01e603

[compiler] Allow manual dependencies to have different optionality than inferred deps (#35186)

Since adding this validation we've already changed our inference to use knowledge from manual memoization to inform when values are frozen and which values are non-nullable. To align with that, if the user chooses to use different optionality btw the deps and the memo block/callback, that's fine. The key is that eg `x?.y` will invalidate whenever `x.y` would, so from a memoization correctness perspective its fine. It's not our job to be a type checker: if a value is potentially nullable, it should likely use a nullable property access in both places but TypeScript/Flow can check that. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35186). * #35201 * #35202 * #35192 * #35190 * __->__ #35186

Joseph Savona committed Nov 24, 2025 at 12:17 UTC 454e01e603464b19ec3b6991a7a781cf1908ac84
3 files changed +44 -39
compiler/packages/babel-plugin-react-compiler/src/Validation/ValidateExhaustiveDependencies.ts
+5 -1
@@ -24,6 +24,7 @@ import {
24 InstructionKind,
25 isStableType,
26 isSubPath,
27 + isSubPathIgnoringOptionals,
28 isUseRefType,
29 LoadGlobal,
30 ManualMemoDependency,
@@ -240,7 +241,10 @@ export function validateExhaustiveDependencies(
241 manualDependency.root.value.identifier.id ===
242 inferredDependency.identifier.id &&
243 (areEqualPaths(manualDependency.path, inferredDependency.path) ||
243 - isSubPath(manualDependency.path, inferredDependency.path))
244 + isSubPathIgnoringOptionals(
245 + manualDependency.path,
246 + inferredDependency.path,
247 + ))
248 ) {
249 hasMatchingManualDependency = true;
250 matched.add(manualDependency);
compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.invalid-exhaustive-deps.expect.md
+32 -38
@@ -9,23 +9,29 @@ import {Stringify} from 'shared-runtime';
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);
@@ -34,6 +40,7 @@ function Component({x, y, z}) {
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 }
@@ -44,7 +51,7 @@ function Component({x, y, z}) {
51 ## Error
52
53 ```
47 -Found 5 errors:
54 +Found 4 errors:
55
56 Error: Found non-exhaustive dependencies
57
@@ -55,61 +62,48 @@ error.invalid-exhaustive-deps.ts:7:11
62 6 | const a = useMemo(() => {
63 > 7 | return x?.y.z?.a;
64 | ^^^^^^^^^ Missing dependency `x?.y.z?.a`
58 - 8 | }, [x?.y.z?.a.b]);
59 - 9 | const b = useMemo(() => {
60 - 10 | return 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 non-exhaustive dependencies
70
71 Missing dependencies can cause a value not to update when those inputs change, resulting in stale UI.
72
66 -error.invalid-exhaustive-deps.ts:10:11
67 - 8 | }, [x?.y.z?.a.b]);
68 - 9 | const b = useMemo(() => {
69 -> 10 | return x.y.z?.a;
70 - | ^^^^^^^^ Missing dependency `x.y.z?.a`
71 - 11 | }, [x.y.z.a]);
72 - 12 | const c = useMemo(() => {
73 - 13 | return x?.y.z.a?.b;
74 -
75 -Error: Found non-exhaustive dependencies
76 -
77 -Missing dependencies can cause a value not to update when those inputs change, resulting in stale UI.
78 -
79 -error.invalid-exhaustive-deps.ts:13:11
80 - 11 | }, [x.y.z.a]);
81 - 12 | const c = useMemo(() => {
82 -> 13 | return x?.y.z.a?.b;
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`
84 - 14 | }, [x?.y.z.a?.b.z]);
85 - 15 | const d = useMemo(() => {
86 - 16 | return x?.y?.[(console.log(y), z?.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, which can cause effects to run more than expected.
85
92 -error.invalid-exhaustive-deps.ts:25:5
93 - 23 | const f = useMemo(() => {
94 - 24 | return [];
95 -> 25 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
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`
97 - 26 | const ref1 = useRef(null);
98 - 27 | const ref2 = useRef(null);
99 - 28 | const ref = z ? ref1 : ref2;
91 + 32 | const ref1 = useRef(null);
92 + 33 | const ref2 = useRef(null);
93 + 34 | const ref = z ? ref1 : ref2;
94
95 Error: Found non-exhaustive dependencies
96
97 Missing dependencies can cause a value not to update when those inputs change, resulting in stale UI.
98
105 -error.invalid-exhaustive-deps.ts:31:13
106 - 29 | const cb = useMemo(() => {
107 - 30 | return () => {
108 -> 31 | return ref.current;
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
110 - 32 | };
111 - 33 | }, []);
112 - 34 | return <Stringify results={[a, b, c, d, e, f, cb]} />;
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/error.invalid-exhaustive-deps.js
+7
@@ -5,23 +5,29 @@ import {Stringify} from 'shared-runtime';
5 function Component({x, y, z}) {
6 const a = useMemo(() => {
7 return x?.y.z?.a;
8 + // error: too precise
9 }, [x?.y.z?.a.b]);
10 const b = useMemo(() => {
11 return x.y.z?.a;
12 + // ok, not our job to type check nullability
13 }, [x.y.z.a]);
14 const c = useMemo(() => {
15 return x?.y.z.a?.b;
16 + // error: too precise
17 }, [x?.y.z.a?.b.z]);
18 const d = useMemo(() => {
19 return x?.y?.[(console.log(y), z?.b)];
20 + // ok
21 }, [x?.y, y, z?.b]);
22 const e = useMemo(() => {
23 const e = [];
24 e.push(x);
25 return e;
26 + // ok
27 }, [x]);
28 const f = useMemo(() => {
29 return [];
30 + // error: unnecessary
31 }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
32 const ref1 = useRef(null);
33 const ref2 = useRef(null);
@@ -30,6 +36,7 @@ function Component({x, y, z}) {
36 return () => {
37 return ref.current;
38 };
39 + // error: ref is a stable type but reactive
40 }, []);
41 return <Stringify results={[a, b, c, d, e, f, cb]} />;
42 }