main
md 376 lines 12.2 KB
Rendered Raw
1 # validateExhaustiveDependencies
2
3 ## File
4 `src/Validation/ValidateExhaustiveDependencies.ts`
5
6 ## Purpose
7 This validation pass ensures that manual memoization (useMemo, useCallback) and effect hooks (useEffect, useLayoutEffect) have correct dependency arrays. The pass compares developer-specified dependencies against the actual values referenced within the memoized function or effect callback to detect:
8
9 1. **Missing dependencies**: Values used in the function that are not listed in the dependency array, causing the memoized value or effect to update less frequently than expected
10 2. **Extra dependencies**: Values listed in the dependency array that are not actually used, causing unnecessary re-computation or effect re-runs
11 3. **Overly precise dependencies**: Dependencies that access deeper property paths than what is actually used (e.g., `x.y.z` when only `x.y` is accessed)
12
13 The goal is to ensure that auto-memoization by the compiler will not substantially change program behavior.
14
15 ## Input Invariants
16 - The function has been through `StartMemoize` and `FinishMemoize` instruction insertion
17 - Manual dependency arrays have been parsed and associated with memoization blocks
18 - Reactive identifiers have been computed
19 - Optional chaining paths have been analyzed
20
21 ## Validation Rules
22 The pass produces errors for:
23
24 1. **Missing dependency in useMemo/useCallback**: A reactive value is used but not listed in deps
25 2. **Extra dependency in useMemo/useCallback**: A value is listed but not used
26 3. **Missing dependency in useEffect**: A value used in the effect callback is not in the deps array
27 4. **Extra dependency in useEffect**: A value in deps is not used in the callback
28 5. **Overly precise dependency**: The manual dep accesses a deeper path than what's actually used
29 6. **Global as dependency**: Module-level values should not be listed as dependencies
30 7. **useEffectEvent in dependency array**: Functions from useEffectEvent must not be in deps
31
32 **Exception - Optional dependencies**: Non-reactive values of stable types (refs, setState) or primitive types are optional and don't need to be listed.
33
34 Error messages produced:
35 - Categories: `MemoDependencies` or `EffectExhaustiveDependencies`
36 - Reasons:
37 - "Found missing memoization dependencies"
38 - "Found extra memoization dependencies"
39 - "Found missing/extra memoization dependencies"
40 - "Found missing effect dependencies"
41 - "Found extra effect dependencies"
42 - "Found missing/extra effect dependencies"
43 - Messages:
44 - "Missing dependency `{dep}`"
45 - "Unnecessary dependency `{dep}`"
46 - "Overly precise dependency `{manual}`, use `{inferred}` instead"
47 - "Functions returned from `useEffectEvent` must not be included in the dependency array"
48 - "Values declared outside of a component/hook should not be listed as dependencies"
49
50 ## Algorithm
51
52 ### Phase 1: Collect Reactive Identifiers
53 Scan all instructions to identify which identifiers are reactive:
54
55 ```typescript
56 function collectReactiveIdentifiersHIR(fn: HIRFunction): Set<IdentifierId> {
57 const reactive = new Set<IdentifierId>();
58 for (const block of fn.body.blocks.values()) {
59 for (const instr of block.instructions) {
60 for (const lvalue of eachInstructionLValue(instr)) {
61 if (lvalue.reactive) {
62 reactive.add(lvalue.identifier.id);
63 }
64 }
65 // ... also check operands
66 }
67 }
68 return reactive;
69 }
70 ```
71
72 ### Phase 2: Find Optional Places
73 Identify places that are within optional chaining expressions:
74
75 ```typescript
76 function findOptionalPlaces(fn: HIRFunction): Map<IdentifierId, boolean> {
77 // Walks through optional terminals to track which identifiers
78 // are accessed via optional chaining (?.property)
79 }
80 ```
81
82 ### Phase 3: Collect Dependencies
83 The core algorithm processes each block, tracking:
84 - `temporaries`: Map of identifier IDs to their dependency information
85 - `locals`: Set of identifiers declared within the current scope
86 - `dependencies`: Set of inferred dependencies
87
88 ```typescript
89 function collectDependencies(
90 fn: HIRFunction,
91 temporaries: Map<IdentifierId, Temporary>,
92 callbacks: {
93 onStartMemoize: (...) => void;
94 onFinishMemoize: (...) => void;
95 onEffect: (...) => void;
96 },
97 isFunctionExpression: boolean,
98 ): Temporary {
99 for (const block of fn.body.blocks.values()) {
100 // Process phi nodes - merge dependencies from control flow
101 for (const phi of block.phis) {
102 // Aggregate dependencies from all operands
103 }
104
105 for (const instr of block.instructions) {
106 switch (value.kind) {
107 case 'LoadLocal':
108 case 'LoadContext':
109 // Track dependency path through the temporary
110 break;
111 case 'PropertyLoad':
112 // Extend dependency path: x -> x.y
113 break;
114 case 'FunctionExpression':
115 // Recursively collect dependencies from nested function
116 break;
117 case 'StartMemoize':
118 // Begin tracking dependencies for this memo block
119 break;
120 case 'FinishMemoize':
121 // Validate collected dependencies against manual deps
122 break;
123 case 'CallExpression':
124 case 'MethodCall':
125 // Check for effect hooks and validate their deps
126 break;
127 }
128 }
129 }
130 }
131 ```
132
133 ### Phase 4: Validate Dependencies
134 Compare inferred dependencies against manual dependencies:
135
136 ```typescript
137 function validateDependencies(
138 inferred: Array<InferredDependency>,
139 manualDependencies: Array<ManualMemoDependency>,
140 reactive: Set<IdentifierId>,
141 ...
142 ): CompilerDiagnostic | null {
143 // Sort and deduplicate inferred dependencies
144 // For each inferred dep, check if there's a matching manual dep
145 // For each manual dep, check if it corresponds to an inferred dep
146 // Report missing and extra dependencies
147 }
148 ```
149
150 ### Dependency Matching Rules
151 - If `x.y.z` is inferred, `x`, `x.y`, or `x.y.z` are valid manual deps
152 - Optional chaining is handled: `x?.y` inferred can match `x.y` manual (ignoring optionals)
153 - Stable types (refs, setState) that are non-reactive are optional
154 - Global values should not be in dependency arrays
155 - useEffectEvent return values should not be in dependency arrays
156
157 ## Edge Cases
158
159 ### Overly Precise Dependency (Error)
160 ```javascript
161 const a = useMemo(() => {
162 return x?.y.z?.a;
163 }, [x?.y.z?.a.b]); // Error: should be [x?.y.z?.a]
164 ```
165
166 ### Unnecessary Dependencies (Error)
167 ```javascript
168 const f = useMemo(() => {
169 return [];
170 }, [x, y.z, GLOBAL]); // Error: all deps are unnecessary
171 ```
172
173 ### Reactive Stable Type (Error)
174 ```javascript
175 const ref1 = useRef(null);
176 const ref2 = useRef(null);
177 const ref = z ? ref1 : ref2; // ref is reactive (depends on z)
178 const cb = useMemo(() => {
179 return () => ref.current;
180 }, []); // Error: missing dep 'ref' (reactive even though stable type)
181 ```
182
183 ### useEffectEvent in Dependencies (Error)
184 ```javascript
185 const effectEvent = useEffectEvent(() => log(x));
186 useEffect(() => {
187 effectEvent();
188 }, [effectEvent]); // Error: useEffectEvent returns should not be in deps
189 ```
190
191 ### Effect with Missing and Extra Dependencies (Error)
192 ```javascript
193 useEffect(() => {
194 log(x, z);
195 }, [x, y]); // Error: missing z, extra y
196 ```
197
198 ### Valid Dependency Specifications
199 ```javascript
200 // All valid - deps cover or exceed what's used
201 const b = useMemo(() => x.y.z?.a, [x.y.z.a]); // OK
202 const d = useMemo(() => x?.y?.[(console.log(y), z?.b)], [x?.y, y, z?.b]); // OK
203 const e = useMemo(() => { e.push(x); return e; }, [x]); // OK
204 ```
205
206 ## Configuration
207 The validation can be configured via compiler options:
208
209 ```typescript
210 // For useMemo/useCallback
211 validateExhaustiveMemoizationDependencies: boolean
212
213 // For useEffect and similar
214 validateExhaustiveEffectDependencies: 'off' | 'all' | 'missing-only' | 'extra-only'
215 ```
216
217 The `missing-only` and `extra-only` modes allow validating only one category of errors.
218
219 ## TODOs
220 From the source file:
221
222 ```typescript
223 /**
224 * TODO: Invalid, Complex Deps
225 *
226 * Handle cases where the user deps were not simple identifiers + property chains.
227 * We try to detect this in ValidateUseMemo but we miss some cases. The problem
228 * is that invalid forms can be value blocks or function calls that don't get
229 * removed by DCE, leaving a structure like:
230 *
231 * StartMemoize
232 * t0 = <value to memoize>
233 * ...non-DCE'd code for manual deps...
234 * FinishMemoize decl=t0
235 */
236 ```
237
238 ## Example
239
240 ### Fixture: `error.invalid-exhaustive-deps.js`
241
242 **Input:**
243 ```javascript
244 // @validateExhaustiveMemoizationDependencies @validateRefAccessDuringRender:false
245 import {useMemo} from 'react';
246
247 function Component({x, y, z}) {
248 const a = useMemo(() => {
249 return x?.y.z?.a;
250 // error: too precise
251 }, [x?.y.z?.a.b]);
252 const f = useMemo(() => {
253 return [];
254 // error: unnecessary
255 }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
256 const ref1 = useRef(null);
257 const ref2 = useRef(null);
258 const ref = z ? ref1 : ref2;
259 const cb = useMemo(() => {
260 return () => ref.current;
261 // error: ref is a stable type but reactive
262 }, []);
263 return <Stringify results={[a, f, cb]} />;
264 }
265 ```
266
267 **Error:**
268 ```
269 Found 4 errors:
270
271 Error: Found missing/extra memoization dependencies
272
273 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.
274
275 error.invalid-exhaustive-deps.ts:7:11
276 5 | function Component({x, y, z}) {
277 6 | const a = useMemo(() => {
278 > 7 | return x?.y.z?.a;
279 | ^^^^^^^^^ Missing dependency `x?.y.z?.a`
280 8 | // error: too precise
281 9 | }, [x?.y.z?.a.b]);
282
283 error.invalid-exhaustive-deps.ts:9:6
284 > 9 | }, [x?.y.z?.a.b]);
285 | ^^^^^^^^^^^ Overly precise dependency `x?.y.z?.a.b`, use `x?.y.z?.a` instead
286
287 Inferred dependencies: `[x?.y.z?.a]`
288
289 Error: Found extra memoization dependencies
290 ...
291 error.invalid-exhaustive-deps.ts:31:6
292 > 31 | }, [x, y.z, z?.y?.a, UNUSED_GLOBAL]);
293 | ^ Unnecessary dependency `x`
294 ...
295 | ^^^^^^^^^^^^^ 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
296
297 Inferred dependencies: `[]`
298
299 Error: Found missing memoization dependencies
300 ...
301 error.invalid-exhaustive-deps.ts:37:13
302 > 37 | return ref.current;
303 | ^^^ 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
304
305 Inferred dependencies: `[ref]`
306 ```
307
308 ### Fixture: `error.invalid-exhaustive-effect-deps.js`
309
310 **Input:**
311 ```javascript
312 // @validateExhaustiveEffectDependencies:"all"
313 import {useEffect} from 'react';
314
315 function Component({x, y, z}) {
316 // error: missing dep - x
317 useEffect(() => {
318 log(x);
319 }, []);
320
321 // error: extra dep - y
322 useEffect(() => {
323 log(x);
324 }, [x, y]);
325
326 // error: missing dep - z; extra dep - y
327 useEffect(() => {
328 log(x, z);
329 }, [x, y]);
330 }
331 ```
332
333 **Error:**
334 ```
335 Found 4 errors:
336
337 Error: Found missing effect dependencies
338
339 Missing dependencies can cause an effect to fire less often than it should.
340
341 error.invalid-exhaustive-effect-deps.ts:7:8
342 > 7 | log(x);
343 | ^ Missing dependency `x`
344
345 Inferred dependencies: `[x]`
346
347 Error: Found extra effect dependencies
348
349 Extra dependencies can cause an effect to fire more often than it should, resulting in performance problems such as excessive renders and side effects.
350
351 error.invalid-exhaustive-effect-deps.ts:13:9
352 > 13 | }, [x, y]);
353 | ^ Unnecessary dependency `y`
354
355 Inferred dependencies: `[x]`
356
357 Error: Found missing/extra effect dependencies
358 ...
359 error.invalid-exhaustive-effect-deps.ts:17:11
360 > 17 | log(x, z);
361 | ^ Missing dependency `z`
362
363 error.invalid-exhaustive-effect-deps.ts:18:9
364 > 18 | }, [x, y]);
365 | ^ Unnecessary dependency `y`
366
367 Inferred dependencies: `[x, z]`
368 ```
369
370 Key observations:
371 - The pass validates both useMemo/useCallback and useEffect dependency arrays
372 - Dependencies are inferred by analyzing actual value usage within the function
373 - Optional chaining paths are tracked and included in dependency paths
374 - Reactive stable types (like conditionally assigned refs) must still be listed
375 - Globals and useEffectEvent returns should not be in dependency arrays
376 - The validation provides fix suggestions showing the inferred correct dependencies