set-state-in-effect
Validates against setting state synchronously in an effect, which can lead to re-renders that degrade performance.
Full Name in eslint-plugin-react-x
react-x/set-state-in-effectFull Name in @eslint-react/eslint-plugin
@eslint-react/set-state-in-effectPresets
x
recommended
recommended-typescript
recommended-type-checked
strict
strict-typescript
strict-type-checked
Rule Details
Setting state immediately inside an effect forces React to restart the entire render cycle. When you update state in an effect, React must re-render your component, apply changes to the DOM, and then run effects again. This creates an extra render pass that could have been avoided by transforming data directly during render or deriving state from props.
Synchronous setState calls in effects trigger immediate re-renders before the browser can paint, causing performance issues and visual jank. React has to render twice: once to apply the state update, then again after effects run. This double rendering is wasteful when the same result could be achieved with a single render.
When something can be calculated from the existing props or state, don't put it in state. Instead, calculate it during rendering. This makes your code faster, simpler, and less error-prone.
In many cases, you may also not need an effect at all. Please see You Might Not Need an Effect for more information.
Examples
Transforming data in effects instead of render
When data can be derived directly from props, putting it in state inside an effect creates an unnecessary render cycle.
// Problem: transforming data in an effect
function Component({ rawData }) {
const [processed, setProcessed] = useState([]);
useEffect(() => {
setProcessed(rawData.map(transform)); // extra render, should compute directly during rendering
}, [rawData]);
}// Recommended: compute directly during rendering
function Component({ rawData }) {
const processed = useMemo(() => rawData.map(transform), [rawData]);
// ...
}// Recommended: simple computations can be done directly during rendering
function Component({ selectedId, items }) {
const selected = items.find((i) => i.id === selectedId);
return <div>{selected?.name}</div>;
}Deriving state from props in effects
Synchronously setting state based on props in an effect is a common anti-pattern that causes extra renders.
// Problem: setting state based on props in an effect
function Component({ selectedId, items }) {
const [selected, setSelected] = useState(null);
useEffect(() => {
setSelected(items.find((i) => i.id === selectedId));
}, [selectedId, items]);
}// Recommended: derive values during rendering
function Component({ selectedId, items }) {
const selection = items.find((item) => item.id === selectedId) ?? null;
// ...
}Resetting state on prop changes
Using an effect to reset state when a prop changes causes an extra render. Instead, use the key prop to reset state automatically.
// Problem: resetting state in an effect
function ProfilePage({ userId }) {
const [comment, setComment] = useState("");
useEffect(() => {
setComment(""); // extra render
}, [userId]);
}// Recommended: use the key prop to reset state automatically
function ProfilePage({ userId }) {
return <Profile userId={userId} key={userId} />;
}
function Profile({ userId }) {
const [comment, setComment] = useState("");
// automatically resets when key changes
}Setting synchronous loading state
Toggling a loading flag synchronously at the start of an effect causes an immediate re-render before any actual work begins.
// Problem: setting loading state synchronously
function Component() {
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true); // synchronously triggers extra render
fetchData().then(() => setLoading(false));
}, []);
}// Recommended: initialize loading state to true so no synchronous setState is needed
function Component() {
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchData().then(() => setLoading(false));
}, []);
}Indirect setState calls are allowed
The rule does not flag indirect calls, such as inside event handlers, async functions, or timers. These run outside of the synchronous effect execution and do not cause the same immediate re-render problem.
// OK: setState in an event handler
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const handler = () => setCount((c) => c + 1);
window.addEventListener("click", handler);
return () => window.removeEventListener("click", handler);
}, []);
}// OK: setState in an async function
function RemoteContent() {
const [content, setContent] = useState("");
useEffect(() => {
let discarded = false;
fetch("https://eslint-react.xyz/content")
.then((resp) => resp.text())
.then((text) => {
if (discarded) return;
setContent(text); // allowed in async callback
});
return () => {
discarded = true;
};
}, []);
}Known Limitations
Cleanup functions
The rule does not check set calls inside useEffect cleanup functions.
// Note: setState in a cleanup function is not flagged
function Component({ firstName, lastName }) {
useEffect(() => {
return () => {
setFullName(firstName + " " + lastName); // not detected
};
}, [firstName, lastName]);
}Async functions before await
The rule does not detect set calls in async functions that are called before the first await statement.
// Note: setState before await in an async function is not flagged
function Component({ data }) {
useEffect(() => {
const fetchData = async () => {
setFullName(data.name); // not detected
await syncToServer(data);
};
fetchData();
}, [data]);
}Options
Shared Settings
Custom state and effect hooks can also be configured via shared ESLint settings, which apply consistently across all rules in the plugin:
{
"settings": {
"react-x": {
"additionalStateHooks": "/^(useMyState|useCustomState)$/u",
"additionalEffectHooks": "/^(useMyEffect|useCustomEffect)$/u"
}
}
}Versions
Resources
Further Reading
- React Docs:
useState - React Docs:
useEffect - React Docs:
useLayoutEffect - React Docs: You Might Not Need an Effect
- React Docs:
set-state-in-effectLint Rule
See Also
react-x/set-state-in-render
Validates against unconditionally setting state during render, which can trigger additional renders and potential infinite render loops.