useEffect cheatsheet.
When NOT to useEffect
The first question is always: do I actually need an effect?
// BAD: derived state via effect
const [fullName, setFullName] = useState("");
useEffect(() => { setFullName(`${first} ${last}`); }, [first, last]);
// GOOD: compute during render
const fullName = `${first} ${last}`;
// BAD: reset state via effect
useEffect(() => { setItem(null); }, [userId]);
// GOOD: key prop
<Component key={userId} /> // remounts on userId change
// BAD: derived data via effect
useEffect(() => { setFiltered(data.filter(p)); }, [data]);
// GOOD: useMemo (or just compute)
const filtered = useMemo(() => data.filter(p), [data]);
Effects are for synchronizing with external systems: DOM, network, timers, browser APIs, subscriptions.
Fetching data
function User({ id }: { id: number }) {
const [data, setData] = useState<User | null>(null);
useEffect(() => {
let cancelled = false;
fetch(`/api/users/${id}`)
.then(r => r.json())
.then((d) => { if (!cancelled) setData(d); });
return () => { cancelled = true; };
}, [id]);
return data ? <p>{data.name}</p> : <p>loading</p>;
}
Or use AbortController:
useEffect(() => {
const ctrl = new AbortController();
fetch(`/api/users/${id}`, { signal: ctrl.signal })
.then(r => r.json())
.then(setData)
.catch((e) => { if (e.name !== "AbortError") throw e; });
return () => ctrl.abort();
}, [id]);
In practice, prefer a data library (TanStack Query, SWR) over rolling your own.
Subscriptions
useEffect(() => {
const handler = (e: MouseEvent) => { ... };
window.addEventListener("click", handler);
return () => window.removeEventListener("click", handler);
}, []);
Timers
useEffect(() => {
const id = setInterval(() => setTime(Date.now()), 1000);
return () => clearInterval(id);
}, []);
Effects that depend on props
useEffect(() => {
doIt(userId);
}, [userId]);
ESLint’s exhaustive-deps rule will tell you what’s missing.
Avoiding stale closure
// Stale: count never updates inside interval
useEffect(() => {
const id = setInterval(() => setCount(count + 1), 1000);
return () => clearInterval(id);
}, []);
// Fix: updater form
useEffect(() => {
const id = setInterval(() => setCount((c) => c + 1), 1000);
return () => clearInterval(id);
}, []);
Event handlers vs effects
Don’t put logic in effects that should fire on user actions:
// BAD
useEffect(() => {
if (submitted) toast("saved");
}, [submitted]);
// GOOD
function onSave() {
doSave();
toast("saved");
}
Effects are for synchronizing with externals, not for reacting to events.
Resetting state on prop change
// BAD: effect to sync
useEffect(() => { setSelected(null); }, [items]);
// BETTER: track previous + reset during render
const [prevItems, setPrevItems] = useState(items);
const [selected, setSelected] = useState(null);
if (items !== prevItems) {
setPrevItems(items);
setSelected(null);
}
Or just use a key prop to remount the component.
Race condition with multiple fetches
useEffect(() => {
let cancelled = false;
fetchData().then(d => { if (!cancelled) setData(d); });
return () => { cancelled = true; };
}, [query]);
Without the cancel flag, fast → slow response order would clobber correct result.
Strict Mode and double-invoke
React 18+ runs effects twice in dev:
useEffect(() => {
console.log("setup");
return () => console.log("cleanup");
}, []);
// Logs: setup, cleanup, setup
Means your effect must be re-runnable safely. If you can’t make it safe, your code has a bug.
Synchronizing with DOM
useLayoutEffect(() => {
const rect = ref.current?.getBoundingClientRect();
setSize({ w: rect.width, h: rect.height });
}, []);
useLayoutEffect runs before browser paint — avoid flicker.
Multiple effects per concern
One effect per concern is clearer than one giant effect:
// GOOD
useEffect(() => { /* subscribe websocket */ }, []);
useEffect(() => { /* sync URL */ }, [filters]);
useEffect(() => { /* doc title */ }, [title]);
useEffectEvent (RFC / experimental)
const onMessage = useEffectEvent((m: Msg) => doStuff(m));
useEffect(() => {
const id = setInterval(() => onMessage(latest), 1000);
return () => clearInterval(id);
}, []);
Lets effects use latest values without becoming deps. Still experimental.
Common mistakes
- Effect to derive state (compute in render).
- Effect with missing deps (exhaustive-deps lint catches).
- Effect fires fetch on every prop change without cancel.
async function setup()directly —useEffectcleanup signature confused. Define + call inside.- Setting state in effect every render — infinite loop.
Read this next
If you want my custom data-fetching hooks, they’re at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .