Autogenerated summaries of each of the compiler passes which allow agents to get the key ideas of a compiler pass, including key input/output invariants, without having to reprocess the file each time. In the subsequent diff this seemed to help. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/facebook/react/pull/35595). * #35607 * #35298 * #35596 * #35573 * __->__ #35595 * #35539
5.6 KiB
validateNoSetStateInEffects
File
src/Validation/ValidateNoSetStateInEffects.ts
Purpose
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.
See: https://react.dev/learn/you-might-not-need-an-effect
Input Invariants
- Operates on HIRFunction (pre-reactive scope inference)
- Effect hooks must be identified (
isUseEffectHookType,isUseLayoutEffectHookType,isUseInsertionEffectHookType) - setState functions must be identified (
isSetStateType) - Only runs when
outputMode === 'lint'
Validation Rules
This pass detects synchronous setState calls within effect bodies:
Standard error message:
Error: Calling setState synchronously within an effect can trigger cascading renders
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:
* Update external systems with the latest state from React.
* Subscribe for updates from some external system, calling setState in a callback function when external state changes.
Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended.
Verbose error message (when enableVerboseNoSetStateInEffect is enabled):
Provides more detailed guidance about specific anti-patterns like non-local derived data, derived event patterns, and force update patterns.
Algorithm
-
Main function traversal: Build a map
setStateFunctionstracking which identifiers are setState functions -
For each instruction:
- LoadLocal/StoreLocal: Propagate setState tracking through variable assignments
- FunctionExpression: Check if the function synchronously calls setState by recursively calling
getSetStateCall(). If so, track the function as a setState-calling function - useEffectEvent call: If the argument is a function that calls setState, track the return value as a setState function
- useEffect/useLayoutEffect/useInsertionEffect call: Check if the callback argument is tracked as calling setState. If so, emit an error
-
getSetStateCall()helper: Recursively analyzes a function to find synchronous setState calls:- Tracks ref-derived values when
enableAllowSetStateFromRefsInEffectsis enabled - Propagates setState tracking through local variables
- Returns the Place of the setState call if found, null otherwise
- Tracks ref-derived values when
Ref-derived setState exception
When enableAllowSetStateFromRefsInEffects is enabled, the pass allows setState calls where:
- The value being set is derived from a ref (
useReforref.current) - The block containing setState is controlled by a ref-dependent condition
This allows patterns like storing initial layout measurements from refs in state.
Edge Cases
Allowed: setState in callbacks
// Valid - setState in event callback, not synchronous
useEffect(() => {
const handler = () => {
setState(newValue);
};
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}, []);
Transitive detection
// Detected - transitive through function calls
const f = () => setState(value);
const g = () => f();
useEffect(() => {
g(); // Error: calls setState transitively
});
useEffectEvent tracking
// Detected - useEffectEvent that calls setState is tracked
const handler = useEffectEvent(() => {
setState(value);
});
useEffect(() => {
handler(); // Error: handler calls setState
});
Allowed: Ref-derived state (with flag)
// Valid when enableAllowSetStateFromRefsInEffects is true
const ref = useRef(null);
useEffect(() => {
const width = ref.current.offsetWidth;
setWidth(width); // Allowed - derived from ref
}, []);
TODOs
From the source code:
/*
* TODO: once we support multiple locations per error, we should link to the
* original Place in the case that setStateFunction.has(callee)
*/
Example
Fixture: invalid-setState-in-useEffect-transitive.js
Input:
// @loggerTestOnly @validateNoSetStateInEffects @outputMode:"lint"
import {useEffect, useState} from 'react';
function Component() {
const [state, setState] = useState(0);
const f = () => {
setState(s => s + 1);
};
const g = () => {
f();
};
useEffect(() => {
g();
});
return state;
}
Error:
Error: Calling setState synchronously within an effect can trigger cascading renders
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:
* Update external systems with the latest state from React.
* Subscribe for updates from some external system, calling setState in a callback function when external state changes.
Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended.
invalid-setState-in-useEffect-transitive.ts:13:4
11 | };
12 | useEffect(() => {
> 13 | g();
| ^ Avoid calling setState() directly within an effect
14 | });
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.