useEffect is the most misused hook in React. Most of the time the bug isn't obvious: the component looks fine, until your API gets hammered or the wrong user's data shows up on screen.

I took four common mistakes, ran each one in real React 19 (rendered with react-dom in a simulated browser), and counted what actually happens. Here are the results and the fix for each.

Prefer video? Here's the 3-minute version:

Mistake 1: an object in the dependency array → infinite loop

function UserList() { const [users, setUsers] = useState([]); const filters = { active: true }; // new object every render! useEffect(() => { fetchUsers(filters).then(setUsers); }, [filters]); return <p>{users.length} users</p>; } 

React compares dependencies by reference. Every render creates a brand-new filters object, so React thinks it changed. The effect runs, calls setUsers, which re-renders, which creates a new object, which runs the effect again. Forever.

I counted the calls with a mocked fetchUsers:

fetchUsers called 15002 times in 1 second ❌ fetchUsers called 1 times in 1 second ✅ (fixed) 

With a real API, that's your server getting hammered.

Fix: keep fresh objects and arrays out of the dependency array.

useEffect(() => { fetchUsers({ active: true }).then(setUsers); }, []); 

Other options: depend on primitive values ([active] instead of [filters]), or wrap the object in useMemo.

Mistake 2: no cleanup → leaks (and a double timer)

useEffect(() => { setInterval(() => { setSeconds(s => s + 1); }, 1000); }, []); 

Nothing stops this interval. When the component unmounts, it keeps running. Here's what I measured inside <StrictMode>:

# without cleanup after 5 seconds: 10s ❌ (two timers!) 3 s after unmount: 6 ticks still running ❌ # with cleanup after 5 seconds: 5s ✅ 3 s after unmount: 0 ticks ✅ 

Why did it count double? In development, Strict Mode intentionally mounts, unmounts and re-mounts your effects to expose missing cleanup. Without cleanup you end up with two intervals.

Fix: return a cleanup function. React calls it before the effect re-runs and when the component unmounts.

useEffect(() => { const id = setInterval(() => { setSeconds(s => s + 1); }, 1000); return () => clearInterval(id); }, []); 

The same applies to event listeners, subscriptions, WebSockets and observers.

Mistake 3: a fetch race condition → wrong data on screen

function Profile({ userId }) { const [user, setUser] = useState(null); useEffect(() => { fetchUser(userId).then(setUser); }, [userId]); return <p>user {userId}: {user?.name}</p>; } 

In my test, user 1's request takes 300 ms and user 2's takes 100 ms. Click user 1, then quickly user 2. Request 2 finishes first, then request 1 finishes and overwrites it:

user 2: Ada ❌ (Ada is user 1!) 

Fix: ignore stale responses with a flag in the cleanup.

useEffect(() => { let ignore = false; fetchUser(userId).then(u => { if (!ignore) setUser(u); }); return () => { ignore = true; }; }, [userId]); 
user 2: Linus ✅ 

When userId changes, the old effect's cleanup sets ignore = true, so its late response is thrown away. You can also cancel the request with an AbortController, or use a data library like TanStack Query, which handles this for you.

Mistake 4: useEffect for derived values → an extra render

const [name, setName] = useState(""); useEffect(() => { setName(first + " " + last); }, [first, last]); 

This renders twice: first with an empty name, then again with the real one. I logged each render:

renders: 2 → "", "Ada Lovelace" ❌ renders: 1 → "Ada Lovelace" ✅ 

That's a wasted render and a flash of wrong content.

Fix: if you can calculate it from props or state, just calculate it during render.

const name = first + " " + last; 

For expensive calculations, use useMemo.

Checklist

  • No new objects or arrays in the dependency array (or useMemo them)
  • Started something? Return a cleanup
  • Fetching? Ignore stale responses (or use AbortController)
  • Can you calculate it during render? Then you don't need an effect
  • Strict Mode runs effects twice in development on purpose, to find these bugs

Which of these have you shipped? 😅 Tell me in the comments.

I post one tested coding tip every day on YouTube at Refactorly. The 30-second version of this one is here.