main
md 150 lines 5.63 KB
Rendered Raw
1 # validateNoSetStateInEffects
2
3 ## File
4 `src/Validation/ValidateNoSetStateInEffects.ts`
5
6 ## Purpose
7 Validates against calling `setState` synchronously in the body of an effect (`useEffect`, `useLayoutEffect`, `useInsertionEffect`), while allowing `setState` in callbacks scheduled by the effect. Synchronous setState in effects triggers cascading re-renders which hurts performance.
8
9 See: https://react.dev/learn/you-might-not-need-an-effect
10
11 ## Input Invariants
12 - Operates on HIRFunction (pre-reactive scope inference)
13 - Effect hooks must be identified (`isUseEffectHookType`, `isUseLayoutEffectHookType`, `isUseInsertionEffectHookType`)
14 - setState functions must be identified (`isSetStateType`)
15 - Only runs when `outputMode === 'lint'`
16
17 ## Validation Rules
18 This pass detects synchronous setState calls within effect bodies:
19
20 **Standard error message:**
21 ```
22 Error: Calling setState synchronously within an effect can trigger cascading renders
23
24 Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:
25 * Update external systems with the latest state from React.
26 * Subscribe for updates from some external system, calling setState in a callback function when external state changes.
27
28 Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended.
29 ```
30
31 **Verbose error message** (when `enableVerboseNoSetStateInEffect` is enabled):
32 Provides more detailed guidance about specific anti-patterns like non-local derived data, derived event patterns, and force update patterns.
33
34 ## Algorithm
35 1. **Main function traversal**: Build a map `setStateFunctions` tracking which identifiers are setState functions
36 2. For each instruction:
37 - **LoadLocal/StoreLocal**: Propagate setState tracking through variable assignments
38 - **FunctionExpression**: Check if the function synchronously calls setState by recursively calling `getSetStateCall()`. If so, track the function as a setState-calling function
39 - **useEffectEvent call**: If the argument is a function that calls setState, track the return value as a setState function
40 - **useEffect/useLayoutEffect/useInsertionEffect call**: Check if the callback argument is tracked as calling setState. If so, emit an error
41
42 3. **`getSetStateCall()` helper**: Recursively analyzes a function to find synchronous setState calls:
43 - Tracks ref-derived values when `enableAllowSetStateFromRefsInEffects` is enabled
44 - Propagates setState tracking through local variables
45 - Returns the Place of the setState call if found, null otherwise
46
47 ### Ref-derived setState exception
48 When `enableAllowSetStateFromRefsInEffects` is enabled, the pass allows setState calls where:
49 - The value being set is derived from a ref (`useRef` or `ref.current`)
50 - The block containing setState is controlled by a ref-dependent condition
51
52 This allows patterns like storing initial layout measurements from refs in state.
53
54 ## Edge Cases
55
56 ### Allowed: setState in callbacks
57 ```javascript
58 // Valid - setState in event callback, not synchronous
59 useEffect(() => {
60 const handler = () => {
61 setState(newValue);
62 };
63 window.addEventListener('resize', handler);
64 return () => window.removeEventListener('resize', handler);
65 }, []);
66 ```
67
68 ### Transitive detection
69 ```javascript
70 // Detected - transitive through function calls
71 const f = () => setState(value);
72 const g = () => f();
73 useEffect(() => {
74 g(); // Error: calls setState transitively
75 });
76 ```
77
78 ### useEffectEvent tracking
79 ```javascript
80 // Detected - useEffectEvent that calls setState is tracked
81 const handler = useEffectEvent(() => {
82 setState(value);
83 });
84 useEffect(() => {
85 handler(); // Error: handler calls setState
86 });
87 ```
88
89 ### Allowed: Ref-derived state (with flag)
90 ```javascript
91 // Valid when enableAllowSetStateFromRefsInEffects is true
92 const ref = useRef(null);
93 useEffect(() => {
94 const width = ref.current.offsetWidth;
95 setWidth(width); // Allowed - derived from ref
96 }, []);
97 ```
98
99 ## TODOs
100 From the source code:
101 ```typescript
102 /*
103 * TODO: once we support multiple locations per error, we should link to the
104 * original Place in the case that setStateFunction.has(callee)
105 */
106 ```
107
108 ## Example
109
110 ### Fixture: `invalid-setState-in-useEffect-transitive.js`
111
112 **Input:**
113 ```javascript
114 // @loggerTestOnly @validateNoSetStateInEffects @outputMode:"lint"
115 import {useEffect, useState} from 'react';
116
117 function Component() {
118 const [state, setState] = useState(0);
119 const f = () => {
120 setState(s => s + 1);
121 };
122 const g = () => {
123 f();
124 };
125 useEffect(() => {
126 g();
127 });
128 return state;
129 }
130 ```
131
132 **Error:**
133 ```
134 Error: Calling setState synchronously within an effect can trigger cascading renders
135
136 Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:
137 * Update external systems with the latest state from React.
138 * Subscribe for updates from some external system, calling setState in a callback function when external state changes.
139
140 Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended.
141
142 invalid-setState-in-useEffect-transitive.ts:13:4
143 11 | };
144 12 | useEffect(() => {
145 > 13 | g();
146 | ^ Avoid calling setState() directly within an effect
147 14 | });
148 ```
149
150 **Why it fails:** Even though `setState` is not called directly in the effect, the pass traces through `g()` -> `f()` -> `setState()` and detects that the effect synchronously triggers a state update.