DEV Community

Cover image for 4 useEffect Mistakes Almost Every React Dev Makes (I Ran Them All in React 19)
Muiduzzaman Lipu for Refactorly

Posted on

4 useEffect Mistakes Almost Every React Dev Makes (I Ran Them All in React 19)

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>;
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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);
}, []);
Enter fullscreen mode Exit fullscreen mode

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);
}, []);
Enter fullscreen mode Exit fullscreen mode

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   ✅
Enter fullscreen mode Exit fullscreen mode

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);
}, []);
Enter fullscreen mode Exit fullscreen mode

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>;
}
Enter fullscreen mode Exit fullscreen mode

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!)
Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode
user 2: Linus   ✅
Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

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"       ✅
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)