set-state-in-effect
Full Name in @eslint-react/eslint-plugin@beta
@eslint-react/set-state-in-effectFull Name in eslint-plugin-react-x@beta
react-x/set-state-in-effectPresets
x
recommended
recommended-typescript
recommended-type-checked
strict
strict-typescript
strict-type-checked
Description
Validates against setting state synchronously in an effect, which can lead to re-renders that degrade performance.
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. Transform data at the top level of your component instead. This code will naturally re-run when props or state change without triggering additional render cycles.
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.
Common Violations
This rule catches several patterns where synchronous setState is used unnecessarily:
- Transforming data in effects instead of render
- Deriving state from props in effects
- Setting loading state synchronously
What is allowed?
The rule does not flag indirect calls, such as:
- Inside event handlers.
- Inside
asyncfunctions. - Inside
setTimeout,setInterval,Promise.then, etc.
Known Limitations
-
It doesn’t check
setcalls inuseEffectcleanup functions.useEffect(() => { return () => { setFullName(firstName + " " + lastName); // ❌ Direct call }; }, [firstName, lastName]); -
It doesn’t detect
setcalls inasyncfunctions that are called before theawaitstatement.useEffect(() => { const fetchData = async () => { setFullName(data.name); // ❌ Direct call }; fetchData(); }, []);
Examples
Passing
The following example from the React Docs demonstrates that setState in an effect is fine if the value comes from a ref, since it cannot be calculated during rendering:
import { useLayoutEffect, useRef, useState } from "react";
function Tooltip() {
const ref = useRef(null);
const [tooltipHeight, setTooltipHeight] = useState(0);
useLayoutEffect(() => {
const { height } = ref.current.getBoundingClientRect();
setTooltipHeight(height);
}, []);
// ...
}The following cases are common valid use cases because they do not call the set function directly in useEffect:
Passing
import { useEffect, useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const handler = () => setCount((c) => c + 1);
window.addEventListener("click", handler);
return () => window.removeEventListener("click", handler);
}, []);
return <h1>{count}</h1>;
}Passing
import { useEffect, useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCount((c) => c + 1);
}, 1000);
return () => clearInterval(intervalId);
}, []);
return <h1>{count}</h1>;
}Passing
import { useEffect, useState } from "react";
export default 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);
});
return () => {
discarded = true;
};
}, []);
return <h1>{count}</h1>;
}TIP
If you need to fetch remote data within the component, consider using libraries like TanStack Query or SWR. They handle caching, re-fetching, and state management for you, making your code cleaner and more efficient.
The following examples are derived from the React Docs: set-state-in-effect and the React Docs: You Might Not Need an Effect:
Failing
import { useEffect, useState } from "react";
// ❌ Synchronous setState in effect
function Component({ data }) {
const [items, setItems] = useState([]);
useEffect(() => {
setItems(data); // Extra render, use initial state instead
}, [data]);
}Failing
import { useEffect, useState } from "react";
// ❌ Setting loading state synchronously
function Component() {
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true); // Synchronous, causes extra render
fetchData().then(() => setLoading(false));
}, []);
}Failing
import { useEffect, useState } from "react";
// ❌ Transforming data in effect
function Component({ rawData }) {
const [processed, setProcessed] = useState([]);
useEffect(() => {
setProcessed(rawData.map(transform)); // Should derive in render
}, [rawData]);
}Passing
import { useMemo, useState } from "react";
// ✅ Calculate during render
function Component({ rawData }) {
const processed = useMemo(() => rawData.map(transform), [rawData]);
// ...
}Failing
import { useEffect, useState } from "react";
// ❌ Deriving state from props
function Component({ selectedId, items }) {
const [selected, setSelected] = useState(null);
useEffect(() => {
setSelected(items.find((i) => i.id === selectedId));
}, [selectedId, items]);
}Passing
// ✅ Calculate during render
function Component({ selectedId, items }) {
const selected = items.find((i) => i.id === selectedId);
return <div>{selected?.name}</div>;
}Failing
import { useEffect, useState } from "react";
export default function ProfilePage({ userId }) {
const [comment, setComment] = useState("");
// 🔴 Avoid: Resetting state on prop change in an Effect
useEffect(() => {
setComment("");
}, [userId]);
// ...
}Passing
import { useState } from "react";
export default function ProfilePage({ userId }) {
return <Profile userId={userId} key={userId} />;
}
function Profile({ userId }) {
// ✅ This and any other state below will reset on key change automatically
const [comment, setComment] = useState("");
// ...
}Failing
import { useEffect, useState } from "react";
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);
// 🔴 Avoid: Adjusting state on prop change in an Effect
useEffect(() => {
setSelection(null);
}, [items]);
// ...
}Passing
import { useState } from "react";
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);
// Better: Adjust the state while rendering
const [prevItems, setPrevItems] = useState(items);
if (items !== prevItems) {
setPrevItems(items);
setSelection(null);
}
// ...
}import { useState } from "react";
function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selectedId, setSelectedId] = useState(null);
// ✅ Best: Calculate everything during rendering
const selection = items.find((item) => item.id === selectedId) ?? null;
// ...
}Implementation
Further Reading
- React Docs:
useStateHook - React Docs:
useEffectHook - React Docs: You Might Not Need an Effect
- React Docs:
set-state-in-effectLint Rule
See Also
set-state-in-render
Validates against unconditionally setting state during render, which can trigger additional renders and potential infinite render loops.