compiler/packages/babel-plugin-react-compiler/docs/passes/46-validateNoSetStateInEffects.md MARKDOWN 151 lines View on github.com → Search inside
1# validateNoSetStateInEffects23## File4`src/Validation/ValidateNoSetStateInEffects.ts`56## Purpose7Validates 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.89See: https://react.dev/learn/you-might-not-need-an-effect1011## Input Invariants12- 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'`1617## Validation Rules18This pass detects synchronous setState calls within effect bodies:1920**Standard error message:**21```22Error: Calling setState synchronously within an effect can trigger cascading renders2324Effects 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.2728Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended.29```3031**Verbose error message** (when `enableVerboseNoSetStateInEffect` is enabled):32Provides more detailed guidance about specific anti-patterns like non-local derived data, derived event patterns, and force update patterns.3334## Algorithm351. **Main function traversal**: Build a map `setStateFunctions` tracking which identifiers are setState functions362. For each instruction:37   - **LoadLocal/StoreLocal**: Propagate setState tracking through variable assignments38   - **FunctionExpression**: Check if the function synchronously calls setState by recursively calling `getSetStateCall()`. If so, track the function as a setState-calling function39   - **useEffectEvent call**: If the argument is a function that calls setState, track the return value as a setState function40   - **useEffect/useLayoutEffect/useInsertionEffect call**: Check if the callback argument is tracked as calling setState. If so, emit an error41423. **`getSetStateCall()` helper**: Recursively analyzes a function to find synchronous setState calls:43   - Tracks ref-derived values when `enableAllowSetStateFromRefsInEffects` is enabled44   - Propagates setState tracking through local variables45   - Returns the Place of the setState call if found, null otherwise4647### Ref-derived setState exception48When `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 condition5152This allows patterns like storing initial layout measurements from refs in state.5354## Edge Cases5556### Allowed: setState in callbacks57```javascript58// Valid - setState in event callback, not synchronous59useEffect(() => {60  const handler = () => {61    setState(newValue);62  };63  window.addEventListener('resize', handler);64  return () => window.removeEventListener('resize', handler);65}, []);66```6768### Transitive detection69```javascript70// Detected - transitive through function calls71const f = () => setState(value);72const g = () => f();73useEffect(() => {74  g(); // Error: calls setState transitively75});76```7778### useEffectEvent tracking79```javascript80// Detected - useEffectEvent that calls setState is tracked81const handler = useEffectEvent(() => {82  setState(value);83});84useEffect(() => {85  handler(); // Error: handler calls setState86});87```8889### Allowed: Ref-derived state (with flag)90```javascript91// Valid when enableAllowSetStateFromRefsInEffects is true92const ref = useRef(null);93useEffect(() => {94  const width = ref.current.offsetWidth;95  setWidth(width); // Allowed - derived from ref96}, []);97```9899## TODOs100From the source code:101```typescript102/*103 * TODO: once we support multiple locations per error, we should link to the104 * original Place in the case that setStateFunction.has(callee)105 */106```107108## Example109110### Fixture: `invalid-setState-in-useEffect-transitive.js`111112**Input:**113```javascript114// @loggerTestOnly @validateNoSetStateInEffects @outputMode:"lint"115import {useEffect, useState} from 'react';116117function 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```131132**Error:**133```134Error: Calling setState synchronously within an effect can trigger cascading renders135136Effects 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.139140Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended.141142invalid-setState-in-useEffect-transitive.ts:13:4143  11 |   };144  12 |   useEffect(() => {145> 13 |     g();146     |     ^ Avoid calling setState() directly within an effect147  14 |   });148```149150**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.

Findings

✓ No findings reported for this file.

Get this view in your editor

Same data, no extra tab — call code_get_file + code_get_findings over MCP from Claude/Cursor/Copilot.