main
md 141 lines 4.96 KB
Rendered Raw
1 # validateNoDerivedComputationsInEffects
2
3 ## File
4 `src/Validation/ValidateNoDerivedComputationsInEffects.ts`
5
6 ## Purpose
7 Validates that `useEffect` is not used for derived computations that could and should be performed during render. This catches a common anti-pattern where developers use effects to synchronize derived state, which causes unnecessary re-renders and complexity.
8
9 See: https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state
10
11 ## Input Invariants
12 - Operates on HIRFunction (pre-reactive scope inference)
13 - Effect hooks must be identified (`isUseEffectHookType`)
14 - setState functions must be identified (`isSetStateType`)
15
16 ## Validation Rules
17 The pass detects when an effect:
18 1. Has a dependency array (2nd argument)
19 2. The effect function only captures the dependencies and setState functions
20 3. The effect calls setState with a value derived solely from the dependencies
21 4. The effect has no control flow (loops with back edges)
22
23 When detected, it produces:
24 ```
25 Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
26 ```
27
28 ## Algorithm
29 1. **Collection Phase**: Traverse all instructions to collect:
30 - `candidateDependencies`: Map of ArrayExpression identifiers (potential deps arrays)
31 - `functions`: Map of FunctionExpression identifiers (potential effect callbacks)
32 - `locals`: Map of LoadLocal sources for identifier resolution
33
34 2. **Detection Phase**: When a `useEffect` call is found with 2 arguments:
35 - Look up the effect function and dependencies array
36 - Verify all dependency array elements are identifiers
37 - Call `validateEffect()` on the effect function
38
39 3. **Effect Validation** (`validateEffect`):
40 - Check that the effect only captures dependencies or setState functions
41 - Check that all dependencies are actually used in the effect
42 - Skip if any block has a back edge (loop)
43 - Track data flow through instructions:
44 - `LoadLocal`: Propagate dependency tracking
45 - `PropertyLoad`, `BinaryExpression`, `TemplateLiteral`, `CallExpression`, `MethodCall`: Aggregate dependencies from operands
46 - When `setState` is called with a single argument that depends on ALL effect dependencies, record the location
47 - If any dependency is used in a terminal operand (control flow), abort validation
48 - Push errors for all recorded setState locations
49
50 ### Value Tracking
51 The pass maintains a `values` map from `IdentifierId` to `Array<IdentifierId>` tracking which effect dependencies each value derives from. When setState is called, if the argument derives from all dependencies, it's flagged as a derived computation.
52
53 ## Edge Cases
54
55 ### Allowed: Effects with side effects
56 ```javascript
57 // Valid - effect captures external values, not just deps
58 useEffect(() => {
59 logToServer(firstName);
60 setFullName(firstName);
61 }, [firstName]);
62 ```
63
64 ### Allowed: Effects with loops
65 ```javascript
66 // Valid - has control flow, not a simple derivation
67 useEffect(() => {
68 let result = '';
69 for (const item of items) {
70 result += item;
71 }
72 setResult(result);
73 }, [items]);
74 ```
75
76 ### Allowed: Effects with conditional setState
77 ```javascript
78 // Valid - setState is conditional on control flow
79 useEffect(() => {
80 if (condition) {
81 setFullName(firstName + lastName);
82 }
83 }, [firstName, lastName]);
84 ```
85
86 ### Not detected: Subset of dependencies
87 ```javascript
88 // Not flagged - only uses firstName, not lastName
89 useEffect(() => {
90 setResult(firstName);
91 }, [firstName, lastName]);
92 ```
93
94 ## TODOs
95 None in source code.
96
97 ## Example
98
99 ### Fixture: `error.invalid-derived-computation-in-effect.js`
100
101 **Input:**
102 ```javascript
103 // @validateNoDerivedComputationsInEffects
104 import {useEffect, useState} from 'react';
105
106 function BadExample() {
107 const [firstName, setFirstName] = useState('Taylor');
108 const [lastName, setLastName] = useState('Swift');
109
110 // Avoid: redundant state and unnecessary Effect
111 const [fullName, setFullName] = useState('');
112 useEffect(() => {
113 setFullName(firstName + ' ' + lastName);
114 }, [firstName, lastName]);
115
116 return <div>{fullName}</div>;
117 }
118 ```
119
120 **Error:**
121 ```
122 Found 1 error:
123
124 Error: Values derived from props and state should be calculated during render, not in an effect. (https://react.dev/learn/you-might-not-need-an-effect#updating-state-based-on-props-or-state)
125
126 error.invalid-derived-computation-in-effect.ts:11:4
127 9 | const [fullName, setFullName] = useState('');
128 10 | useEffect(() => {
129 > 11 | setFullName(firstName + ' ' + lastName);
130 | ^^^^^^^^^^^ Values derived from props and state should be calculated during render, not in an effect.
131 12 | }, [firstName, lastName]);
132 13 |
133 14 | return <div>{fullName}</div>;
134 ```
135
136 **Why it fails:** The effect computes `fullName` purely from `firstName` and `lastName` (the dependencies) and then sets state. This is a derived computation that should be calculated during render:
137
138 ```javascript
139 // Correct approach
140 const fullName = firstName + ' ' + lastName;
141 ```