main
md 330 lines 9.86 KB
Rendered Raw
1 # validateHooksUsage
2
3 ## File
4 `src/Validation/ValidateHooksUsage.ts`
5
6 ## Purpose
7 This validation pass ensures that the function honors the [Rules of Hooks](https://react.dev/warnings/invalid-hook-call-warning). Specifically, it validates that:
8
9 1. Hooks may only be called unconditionally (not in if statements, loops, etc.)
10 2. Hooks cannot be used as first-class values (passed around, stored in variables, etc.)
11 3. Hooks must be the same function on every render (no dynamic hooks)
12 4. Hooks must be called at the top level, not within nested function expressions
13
14 ## Input Invariants
15 - The function has been lowered to HIR
16 - Global bindings have been resolved and typed
17 - Nested function expressions have been lowered
18
19 ## Value Kinds Lattice
20
21 The pass uses abstract interpretation with a lattice of value kinds:
22
23 ```typescript
24 enum Kind {
25 Error, // Hook already used in an invalid way (stop reporting)
26 KnownHook, // Definitely a hook (from LoadGlobal with hook type)
27 PotentialHook, // Might be a hook (hook-like name but not from global)
28 Global, // A global value that is not a hook
29 Local, // A local variable
30 }
31 ```
32
33 The `joinKinds` function merges kinds, with earlier kinds taking precedence:
34 - `Error` > `KnownHook` > `PotentialHook` > `Global` > `Local`
35
36 ## Validation Rules
37
38 ### Rule 1: No Conditional Hook Calls
39 Hooks must always be called in a consistent order.
40
41 **Error:**
42 ```
43 Error: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
44 ```
45
46 ### Rule 2: No Hooks as First-Class Values
47 Known hooks may not be referenced as normal values (only called).
48
49 **Error:**
50 ```
51 Error: Hooks may not be referenced as normal values, they must be called. See https://react.dev/reference/rules/react-calls-components-and-hooks#never-pass-around-hooks-as-regular-values
52 ```
53
54 ### Rule 3: No Dynamic Hooks
55 Potential hooks (hook-like names from local scope) may change between renders.
56
57 **Error:**
58 ```
59 Error: Hooks must be the same function on every render, but this value may change over time to a different function. See https://react.dev/reference/rules/react-calls-components-and-hooks#dont-dynamically-use-hooks
60 ```
61
62 ### Rule 4: No Hooks in Nested Functions
63 Hooks must be called at the top level of a component or custom hook.
64
65 **Error:**
66 ```
67 Error: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
68
69 Cannot call [hookKind] within a function expression
70 ```
71
72 ## Algorithm
73
74 ### Phase 1: Compute Unconditional Blocks
75 ```typescript
76 const unconditionalBlocks = computeUnconditionalBlocks(fn);
77 ```
78 Determines which blocks are guaranteed to execute on every render (not inside conditionals).
79
80 ### Phase 2: Initialize Tracking
81 ```typescript
82 const valueKinds = new Map<IdentifierId, Kind>();
83
84 // Initialize parameters
85 for (const param of fn.params) {
86 const place = param.kind === 'Identifier' ? param : param.place;
87 const kind = getKindForPlace(place); // PotentialHook if hook-like name
88 setKind(place, kind);
89 }
90 ```
91
92 ### Phase 3: Track Value Kinds Through Instructions
93
94 For each instruction, the pass tracks how hook-ness flows through values:
95
96 ```typescript
97 case 'LoadGlobal':
98 // Globals are the source of KnownHook
99 if (getHookKind(fn.env, instr.lvalue.identifier) != null) {
100 setKind(instr.lvalue, Kind.KnownHook);
101 } else {
102 setKind(instr.lvalue, Kind.Global);
103 }
104 break;
105
106 case 'PropertyLoad':
107 // Hook-like property of Global -> KnownHook
108 // Hook-like property of Local -> PotentialHook
109 // Property of KnownHook -> KnownHook (if hook-like name)
110 const objectKind = getKindForPlace(value.object);
111 const isHookProperty = isHookName(value.property);
112 // Determine kind based on object kind and property name
113 break;
114
115 case 'CallExpression':
116 const calleeKind = getKindForPlace(value.callee);
117 const isHookCallee = calleeKind === Kind.KnownHook || calleeKind === Kind.PotentialHook;
118
119 if (isHookCallee && !unconditionalBlocks.has(block.id)) {
120 recordConditionalHookError(value.callee);
121 } else if (calleeKind === Kind.PotentialHook) {
122 recordDynamicHookUsageError(value.callee);
123 }
124 break;
125 ```
126
127 ### Phase 4: Check for Invalid Hook References
128
129 When a `KnownHook` is used as an operand (not as a callee), it's an error:
130
131 ```typescript
132 function visitPlace(place: Place): void {
133 const kind = valueKinds.get(place.identifier.id);
134 if (kind === Kind.KnownHook) {
135 recordInvalidHookUsageError(place);
136 }
137 }
138 ```
139
140 ### Phase 5: Validate Nested Function Expressions
141
142 Recursively check that nested functions don't call hooks:
143
144 ```typescript
145 function visitFunctionExpression(errors: CompilerError, fn: HIRFunction) {
146 for (const instr of allInstructions(fn)) {
147 if (isCall(instr)) {
148 const callee = getCallee(instr);
149 const hookKind = getHookKind(fn.env, callee.identifier);
150 if (hookKind != null) {
151 errors.push({
152 reason: 'Hooks must be called at the top level...',
153 description: `Cannot call ${hookKind} within a function expression`,
154 });
155 }
156 }
157 // Recursively check nested functions
158 if (isFunctionExpression(instr)) {
159 visitFunctionExpression(errors, instr.value.loweredFunc.func);
160 }
161 }
162 }
163 ```
164
165 ### Phi Node Handling
166
167 For phi nodes (control flow join points), the pass joins the kinds of all operands:
168
169 ```typescript
170 for (const phi of block.phis) {
171 let kind = isHookName(phi.place.identifier.name) ? Kind.PotentialHook : Kind.Local;
172 for (const [, operand] of phi.operands) {
173 const operandKind = valueKinds.get(operand.identifier.id);
174 if (operandKind !== undefined) {
175 kind = joinKinds(kind, operandKind);
176 }
177 }
178 valueKinds.set(phi.place.identifier.id, kind);
179 }
180 ```
181
182 ## Edge Cases
183
184 ### Optional Calls
185 Optional calls like `useHook?.()` are treated as conditional:
186 ```javascript
187 const result = useHook?.(); // Error: conditional hook call
188 ```
189
190 ### Property Access on Hooks
191 Hook-like properties of known hooks are also known hooks:
192 ```javascript
193 const useFoo = useHook.useFoo; // useFoo is KnownHook
194 useFoo(); // Must be called unconditionally
195 ```
196
197 ### Destructuring from Global
198 Destructuring hook-like names from a global creates known hooks:
199 ```javascript
200 const {useState} = React; // useState is KnownHook
201 ```
202
203 ### Hook-Like Names from Local Variables
204 Hook-like names from local variables are potential hooks:
205 ```javascript
206 const obj = createObject();
207 const useFoo = obj.useFoo; // PotentialHook
208 useFoo(); // Error: dynamic hook
209 ```
210
211 ### Error Deduplication
212 The pass deduplicates errors by source location, and once an error is recorded for a place, it's marked as `Kind.Error` to prevent further errors for the same place.
213
214 ## TODOs
215
216 1. **Fixpoint iteration for loops** - The pass currently skips phi operands whose value is unknown (which can occur in loops). A follow-up could expand this to fixpoint iteration:
217 ```typescript
218 // NOTE: we currently skip operands whose value is unknown
219 // (which can only occur for functions with loops), we may
220 // cause us to miss invalid code in some cases. We should
221 // expand this to a fixpoint iteration in a follow-up.
222 ```
223
224 ## Example
225
226 ### Fixture: `rules-of-hooks/error.invalid-hook-if-consequent.js`
227
228 **Input:**
229 ```javascript
230 function Component(props) {
231 let x = null;
232 if (props.cond) {
233 x = useHook();
234 }
235 return x;
236 }
237 ```
238
239 **Error:**
240 ```
241 Error: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
242
243 error.invalid-hook-if-consequent.ts:4:8
244 2 | let x = null;
245 3 | if (props.cond) {
246 > 4 | x = useHook();
247 | ^^^^^^^ Hooks must always be called in a consistent order...
248 5 | }
249 6 | return x;
250 ```
251
252 ### Fixture: `rules-of-hooks/error.invalid-hook-as-prop.js`
253
254 **Input:**
255 ```javascript
256 function Component({useFoo}) {
257 useFoo();
258 }
259 ```
260
261 **Error:**
262 ```
263 Error: Hooks must be the same function on every render, but this value may change over time to a different function. See https://react.dev/reference/rules/react-calls-components-and-hooks#dont-dynamically-use-hooks
264
265 error.invalid-hook-as-prop.ts:2:2
266 1 | function Component({useFoo}) {
267 > 2 | useFoo();
268 | ^^^^^^ Hooks must be the same function on every render...
269 3 | }
270 ```
271
272 ### Fixture: `rules-of-hooks/error.invalid-hook-in-nested-function-expression-object-expression.js`
273
274 **Input:**
275 ```javascript
276 function Component() {
277 'use memo';
278 const f = () => {
279 const x = {
280 outer() {
281 const g = () => {
282 const y = {
283 inner() {
284 return useFoo();
285 },
286 };
287 return y;
288 };
289 },
290 };
291 return x;
292 };
293 }
294 ```
295
296 **Error:**
297 ```
298 Error: Hooks must be called at the top level in the body of a function component or custom hook, and may not be called within function expressions. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
299
300 Cannot call hook within a function expression.
301
302 error.invalid-hook-in-nested-function-expression-object-expression.ts:10:21
303 8 | const y = {
304 9 | inner() {
305 > 10 | return useFoo();
306 | ^^^^^^ Hooks must be called at the top level...
307 11 | },
308 12 | };
309 ```
310
311 ### Fixture: `rules-of-hooks/error.invalid-hook-optionalcall.js`
312
313 **Input:**
314 ```javascript
315 function Component() {
316 const {result} = useConditionalHook?.() ?? {};
317 return result;
318 }
319 ```
320
321 **Error:**
322 ```
323 Error: Hooks must always be called in a consistent order, and may not be called conditionally. See the Rules of Hooks (https://react.dev/warnings/invalid-hook-call-warning)
324
325 error.invalid-hook-optionalcall.ts:2:19
326 1 | function Component() {
327 > 2 | const {result} = useConditionalHook?.() ?? {};
328 | ^^^^^^^^^^^^^^^^^^ Hooks must always be called in a consistent order...
329 3 | return result;
330 ```