← Back to all posts
Cover image for You're Probably Using useEffect Wrong

You're Probably Using useEffect Wrong

hooksreactweb development

If you’re like most React devs, your first dance with useEffect probably went something like this:

useEffect(() => {
  // do the thing
}, []);

Then someone said, “Hey, don’t forget your dependencies,” so you added a few things:

useEffect(() => {
  doSomething(data);
}, [data]);

Then things got weird. It fired too often. Or not often enough. Or twice in dev mode. Or caused an infinite loop. So you did what every dev does when confronted with the unknown: you started guessing.

Let’s un-guess some things.


What useEffect Actually Is

useEffect is React's way of syncing side effects with state or prop changes. It’s basically saying:

“When these specific values change, run this function. But only then.”

It’s not a lifecycle method. It’s not a data-fetching hook. It’s just a sync tool. But because it's so flexible, people end up using it for everything—and that's where things go sideways.


Common Pitfall: Running Effects That Don't Need to Be Effects

Let’s say you’re updating the document title:

useEffect(() => {
  document.title = `Hello, ${name}`;
}, [name]);

This works. But it’s synchronous. It doesn't need to be a side effect unless you're reacting to async data or triggering something outside React's render cycle.

A cleaner version might just be:

document.title = `Hello, ${name}`;

Inside the component body, during render. That’s perfectly valid if it’s simple and doesn’t require cleanup.


What Actually Belongs in useEffect?

Here’s your gut check:

“Does this code need to happen after render, or in response to something changing?”

If yes, it's a good use case. If no, you're probably cramming logic into useEffect that could live elsewhere.

Examples that do belong:

  • Fetching data on mount
  • Subscribing to a service or event
  • Setting timeouts/intervals
  • Manually changing the DOM (with third-party libs, etc.)
  • Anything that needs cleanup (e.g. returning a function from useEffect)

The Double Render in Dev Mode (You're Not Crazy)

React’s Strict Mode in development will call your effect twice. Not a bug—it’s intentional. It wants to make sure your effect logic is pure and doesn’t have unintended consequences. But it makes things like API calls super annoying.

If you're seeing duplicate fetches, use a flag or an abort controller:

useEffect(() => {
  let ignore = false;

  async function fetchData() {
    const res = await fetch("/api/data");
    const data = await res.json();
    if (!ignore) setData(data);
  }

  fetchData();
  return () => { ignore = true; };
}, []);

The Cleanup Pattern

If your effect sets up anything that persists (like a subscription, interval, or event listener), clean it up:

useEffect(() => {
  const id = setInterval(() => {
    console.log("tick");
  }, 1000);

  return () => clearInterval(id); // 🔥 don't forget this
}, []);

Without cleanup, you're gonna have a bad time.


TL;DR Rules for useEffect

  • Don’t use it just because you “might need it later.”
  • If it can be done during render, do it there.
  • Keep effects small and focused—split them if needed.
  • Use cleanup religiously.
  • Respect the dependencies array (no cheating with // eslint-disable-line unless you're really sure).

That’s it. Try removing one useEffect from your project today. You might not need it after all.