main
md 133 lines 4.78 KB
Rendered Raw
1 # validateNoSetStateInRender
2
3 ## File
4 `src/Validation/ValidateNoSetStateInRender.ts`
5
6 ## Purpose
7 Validates that a component does not unconditionally call `setState` during render, which would cause an infinite update loop. This pass is conservative and may miss some cases (false negatives) but avoids false positives.
8
9 ## Input Invariants
10 - Operates on HIRFunction (pre-reactive scope inference)
11 - Must run before reactive scope inference
12 - Uses `computeUnconditionalBlocks` to determine which blocks always execute
13
14 ## Validation Rules
15 This pass detects two types of violations:
16
17 1. **Unconditional setState in render**: Calling `setState` (or a function that transitively calls setState) in a block that always executes during render.
18
19 2. **setState inside useMemo**: Calling `setState` inside a `useMemo` callback, which can cause infinite loops when the memo's dependencies change.
20
21 ### Error Messages
22
23 **For unconditional setState in render:**
24 ```
25 Error: Cannot call setState during render
26
27 Calling setState during render may trigger an infinite loop.
28 * To reset state when other state/props change, store the previous value in state and update conditionally: https://react.dev/reference/react/useState#storing-information-from-previous-renders
29 * To derive data from other state/props, compute the derived data during render without using state
30 ```
31
32 **For setState in useMemo:**
33 ```
34 Error: Calling setState from useMemo may trigger an infinite loop
35
36 Each time the memo callback is evaluated it will change state. This can cause a memoization dependency to change, running the memo function again and causing an infinite loop. Instead of setting state in useMemo(), prefer deriving the value during render.
37 ```
38
39 ## Algorithm
40 1. Compute the set of unconditional blocks using post-dominator analysis
41 2. Initialize a set `unconditionalSetStateFunctions` to track functions that unconditionally call setState
42 3. Traverse all blocks and instructions:
43 - **LoadLocal/StoreLocal**: Propagate setState tracking through variable assignments and loads
44 - **FunctionExpression/ObjectMethod**: Recursively check if the function unconditionally calls setState. If so, add the function's lvalue to the tracking set
45 - **StartMemoize/FinishMemoize**: Track when inside a manual memoization block (useMemo/useCallback)
46 - **CallExpression**: Check if the callee is a setState function or tracked setter:
47 - If inside a memoize block, emit a useMemo-specific error
48 - If in an unconditional block, emit a render-time setState error
49
50 ### Key Helper: `computeUnconditionalBlocks`
51 Uses post-dominator tree analysis to find blocks that always execute when the function runs. The analysis ignores throw statements since hooks only need consistent ordering for normal execution paths.
52
53 ## Edge Cases
54
55 ### Conditional setState is allowed
56 ```javascript
57 // This is valid - setState is conditional
58 if (someCondition) {
59 setState(newValue);
60 }
61 ```
62
63 ### Transitive detection through functions
64 ```javascript
65 // Detected - setTrue unconditionally calls setState
66 const setTrue = () => setState(true);
67 setTrue(); // Error here
68 ```
69
70 ### False negative: setState in data structures
71 ```javascript
72 // NOT detected - setState stored in array then extracted
73 const [state, setState] = useState(false);
74 const x = [setState];
75 const y = x.pop();
76 y(); // No error, but will cause infinite loop
77 ```
78
79 ### Feature flag: enableUseKeyedState
80 When enabled, the error message suggests using `useKeyedState(initialState, key)` as an alternative pattern for resetting state when dependencies change.
81
82 ## TODOs
83 None in source code.
84
85 ## Example
86
87 ### Fixture: `error.invalid-unconditional-set-state-in-render.js`
88
89 **Input:**
90 ```javascript
91 // @validateNoSetStateInRender
92 function Component(props) {
93 const [x, setX] = useState(0);
94 const aliased = setX;
95
96 setX(1);
97 aliased(2);
98
99 return x;
100 }
101 ```
102
103 **Error:**
104 ```
105 Found 2 errors:
106
107 Error: Cannot call setState during render
108
109 Calling setState during render may trigger an infinite loop.
110 * To reset state when other state/props change, store the previous value in state and update conditionally: https://react.dev/reference/react/useState#storing-information-from-previous-renders
111 * To derive data from other state/props, compute the derived data during render without using state.
112
113 error.invalid-unconditional-set-state-in-render.ts:6:2
114 4 | const aliased = setX;
115 5 |
116 > 6 | setX(1);
117 | ^^^^ Found setState() in render
118 7 | aliased(2);
119 8 |
120 9 | return x;
121
122 Error: Cannot call setState during render
123
124 ...
125
126 error.invalid-unconditional-set-state-in-render.ts:7:2
127 5 |
128 6 | setX(1);
129 > 7 | aliased(2);
130 | ^^^^^^^ Found setState() in render
131 ```
132
133 **Why it fails:** Both `setX(1)` and `aliased(2)` are unconditionally called during render. The pass tracks that `aliased` is assigned from `setX`, so calling `aliased()` is also detected as a setState call.