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 — useEffect cleanup 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 .