Ticker

6/recent/ticker-posts

⚡ React useEffect – Stop Running Side Effects at the Wrong Time

⚡ React useEffect

😵 The Problem

You're building a React component.

You need to fetch some data when the component appears:

function Users() {
  // fetch users here
}

Easy, right?

So you might try:

function Users() {
  fetch("/api/users");

  return <h1>Users</h1>;
}

Looks innocent.

But there's a problem.

Every time the component renders…

👉 the fetch() runs again.

And if your component re-renders because of state changes?

Yep.

Another request.

And another.

And another. 💀

This is exactly the kind of problem useEffect is designed to solve.

💡 The Solution: useEffect

useEffect lets you run code after React has rendered your component.

For example:

import { useEffect } from "react";

function Users() {
  useEffect(() => {
    fetch("/api/users");
  }, []);

  return <h1>Users</h1>;
}

The important part is:

useEffect(() => {
  // side effect
}, []);

You're basically telling React:

"After rendering this component, run this side effect."

That side effect could be:

  • fetching data
  • changing the document title
  • subscribing to something
  • setting up timers
  • interacting with browser APIs
  • connecting to external systems

🧠 First, What Is a "Side Effect"?

This term sounds more complicated than it really is.

Rendering should ideally be predictable.

For example:

function User({ name }) {
  return <h2>{name}</h2>;
}

That's just UI calculation.

But this:

document.title = "Dashboard";

changes something outside the React component.

Or:

fetch("/api/users");

talks to an external system.

Those are side effects.

A useful mental model:

Rendering calculates UI. Effects synchronize with things outside React.

That distinction becomes really important as your app grows.

🔥 The Dependency Array Is the Important Part

You'll usually see three versions.

1. No Dependency Array

useEffect(() => {
  console.log("Effect ran");
});

This runs after every render.

Sometimes that's intentional.

Most of the time, though, you should stop and ask why.

2. Empty Dependency Array

useEffect(() => {
  console.log("Effect ran");
}, []);

This tells React that the effect doesn't depend on changing reactive values.

It runs after the initial mount in normal usage.

But there's an important development detail: with React Strict Mode, React may intentionally run setup and cleanup an extra time in development to help detect unsafe effects.

So don't build logic that assumes an effect literally runs only once forever.

3. Dependencies

useEffect(() => {
  console.log("User changed:", userId);
}, [userId]);

Now the effect runs when userId changes.

This is where useEffect becomes genuinely useful.

🎯 Real Example: Fetching Data

Let's say your page loads products based on a category.

import { useEffect, useState } from "react";

function Products({ category }) {
  const [products, setProducts] = useState([]);

  useEffect(() => {
    async function loadProducts() {
      const response = await fetch(
        `/api/products?category=${category}`
      );

      const data = await response.json();

      setProducts(data);
    }

    loadProducts();
  }, [category]);

  return (
    <div>
      {products.map(product => (
        <p key={product.id}>
          {product.name}
        </p>
      ))}
    </div>
  );
}

Now the relationship is clear:

category changes
       ↓
useEffect runs
       ↓
fetch new products
       ↓
setProducts()
       ↓
component renders with new data

That's a much better mental model than:

"useEffect is where I put code that should run."

Instead ask:

"What external system am I synchronizing with, and what values should cause that synchronization to happen?"

🔥 Real Example: Updating the Page Title

You don't need an API to understand useEffect.

function Profile({ name }) {
  useEffect(() => {
    document.title = `${name}'s Profile`;
  }, [name]);

  return <h1>{name}</h1>;
}

Whenever name changes:

name changes
    ↓
React renders
    ↓
effect runs
    ↓
document.title updates

Simple.

And this is a good example of a genuine effect because document.title lives outside React.

⏱️ Real Example: Timers

Imagine a countdown.

You could create an interval inside an effect:

import { useEffect, useState } from "react";

function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const intervalId = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);

    return () => {
      clearInterval(intervalId);
    };
  }, []);

  return <h2>{seconds}s</h2>;
}

Notice the return.

That's the cleanup function.

🧹 Cleanup: The Part People Forget

Some effects create resources that need to be removed.

Examples:

  • intervals
  • event listeners
  • subscriptions
  • WebSocket connections

If you create one:

const intervalId = setInterval(...);

you should clean it up:

return () => {
  clearInterval(intervalId);
};

The mental model is:

Effect starts something
        ↓
Component stays active
        ↓
Component unmounts / effect reruns
        ↓
Cleanup removes the old thing

Without cleanup, you can end up with memory leaks, duplicate listeners, duplicate connections, or stale behavior.

💡 Real Developer Insight

Here's where I see developers misuse `useEffect the most:

They use it for things that don't need an effect.

For example:

const fullName = `${firstName} ${lastName}`;

You don't need:

useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);

That's unnecessary state + unnecessary effect.

Just calculate it:

const fullName = `${firstName} ${lastName}`;

Same result.

Less code.

Less synchronization.

Less chance of bugs.

Another common example:

const filteredUsers = users.filter(
  user => user.active
);

You usually don't need an effect to calculate this either.

useEffect isn't a general-purpose "run this code later" hook.

Use it when you're synchronizing React with something outside React.

That's the mindset that makes useEffect much easier to use correctly.

⚠️ Common Developer Mistakes

1. Forgetting Dependencies

useEffect(() => {
  fetch(`/api/users/${userId}`);
}, []);

If userId can change, the effect is now using stale data.

You likely need:

useEffect(() => {
  fetch(`/api/users/${userId}`);
}, [userId]);

2. Creating Effects for Derived Values

Don't do this:

useEffect(() => {
  setTotal(price * quantity);
}, [price, quantity]);

If total can simply be calculated:

const total = price * quantity;

That's cleaner.

3. Forgetting Cleanup

Bad:

useEffect(() => {
  window.addEventListener("resize", handleResize);
}, []);

Better:

useEffect(() => {
  window.addEventListener("resize", handleResize);

  return () => {
    window.removeEventListener("resize", handleResize);
  };
}, []);

4. Ignoring Race Conditions in Data Fetching

Imagine:

Request A → slow
Request B → fast

B returns first.

Then A finally returns and overwrites the newer data.

For real applications, async effects may need cancellation or an "ignore stale result" strategy.

For example:

useEffect(() => {
  const controller = new AbortController();

  async function loadUser() {
    try {
      const response = await fetch(
        `/api/users/${userId}`,
        { signal: controller.signal }
      );

      const data = await response.json();

      setUser(data);
    } catch (error) {
      if (error.name !== "AbortError") {
        console.error(error);
      }
    }
  }

  loadUser();

  return () => {
    controller.abort();
  };
}, [userId]);

That's much closer to how you'd approach data fetching in a production application.

🧠 One More Important Thing: Don't Make the Effect Function async

You might be tempted to write:

useEffect(async () => {
  // ...
}, []);

Don't.

The effect callback is expected to return either nothing or a cleanup function, not a Promise.

Instead:

useEffect(() => {
  async function loadData() {
    // async work
  }

  loadData();
}, []);

This small distinction saves you from some very confusing behavior.

🚀 Best Practice Summary

✅ Use useEffect to synchronize with systems outside React
✅ Keep dependency arrays accurate instead of trying to silence them
✅ Clean up subscriptions, timers, listeners, and connections
✅ Don't use useEffect for values that can be calculated during render
✅ Handle async effects carefully, especially when requests can overlap

🎯 Conclusion

useEffect is one of those React features that looks simple:

useEffect(() => {
  // something
}, []);

Then you build a real application…

…and suddenly you're staring at five effects wondering:

"Why is this running again?" 😅

The trick isn't memorizing dependency-array rules.

Understand the reason the effect exists.

Ask:

"What external thing am I synchronizing with?"

If the answer is:

  • API
  • browser API
  • timer
  • event listener
  • subscription
  • external system

then useEffect may be exactly what you need.

But if you're just calculating a value from existing props or state…

You probably don't need an effect.

Less effects usually means less complexity. 🚀

Reactions

Post a Comment

0 Comments