| 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. |