DEV Community

Cover image for Why We Use useEffect in React - Explained Simply
DHANRAJ S
DHANRAJ S

Posted on

Why We Use useEffect in React - Explained Simply

Hey!

Let me ask you something.

You are building a React component. When the page loads — you want to fetch some data from an API.

So you try this:

function UserProfile() {
  const [user, setUser] = useState(null);

  fetch("https://api.example.com/user")
    .then(res => res.json())
    .then(data => setUser(data));

  return <div>{user ? user.name : "Loading..."}</div>;
}
Enter fullscreen mode Exit fullscreen mode

Run it. Check your network tab.

The fetch is firing again. And again. And again. Infinite loop.

Every fetch updates state. Every state update re-renders the component. Every re-render calls fetch again. It never stops.

That is the problem. And useEffect is the solution.


1. Why Does This Happen?

Every time state changes — React re-renders the component. That means every line of code inside the function runs again from top to bottom.

So the fetch runs. It updates state. State triggers a re-render. Fetch runs again. Forever.

For things like fetching data or setting up a timer — you do not want them running on every render. You want them to run at specific times.

That is exactly what useEffect does.


2. What Is useEffect?

useEffect is a React hook that lets you run code at a specific time — not on every render.

Think of it like a trigger. You tell React — "when this specific thing happens, run this code."

import { useEffect } from "react";

useEffect(() => {
  // code you want to run
}, [dependencies]);
Enter fullscreen mode Exit fullscreen mode

Let us break this down.

() => { } — the code you want to run. This is called the effect.

[dependencies] — the dependency array. This controls when the effect runs. This is the most important part.


3. The Dependency Array — How It Controls useEffect

There are three ways to write it. Each behaves differently.

No array — runs after every render

useEffect(() => {
  console.log("runs every render");
});
Enter fullscreen mode Exit fullscreen mode

Rarely what you want.

Empty array — runs only once on mount

useEffect(() => {
  console.log("runs once when component loads");
}, []);
Enter fullscreen mode Exit fullscreen mode

Perfect for fetching data when the page loads.

Array with a value — runs when that value changes

useEffect(() => {
  console.log("runs when count changes");
}, [count]);
Enter fullscreen mode Exit fullscreen mode

Runs once on mount — and again every time count changes.

Quick question for you.

If you want to fetch user data only once when the page loads — which one should you use?

Empty array. The fetch runs once. Data comes back. State updates. Screen shows the result. No loop.


4. Fix the Infinite Loop

Now let us fix the broken code from the beginning.

// Broken
function UserProfile() {
  const [user, setUser] = useState(null);

  fetch("https://api.example.com/user")
    .then(res => res.json())
    .then(data => setUser(data));

  return <div>{user ? user.name : "Loading..."}</div>;
}
Enter fullscreen mode Exit fullscreen mode
// Fixed
function UserProfile() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch("https://api.example.com/user")
      .then(res => res.json())
      .then(data => setUser(data));
  }, []);

  return <div>{user ? user.name : "Loading..."}</div>;
}
Enter fullscreen mode Exit fullscreen mode

The fetch is now inside useEffect with an empty array.

Runs once. Gets the data. Updates state. Screen shows the name. No loop.

One small change. Completely different behavior.


5. The Component Lifecycle

Every React component goes through three stages.

Mount — the component appears on screen for the first time.

Update — the component re-renders because state or props changed.

Unmount — the component is removed from the screen.

useEffect connects to all three stages.

Component mounts
  → useEffect with [] runs here

State changes
  → Component updates
  → useEffect with [value] runs here

Component unmounts
  → Cleanup function inside useEffect runs here
Enter fullscreen mode Exit fullscreen mode

6. Example 1 — Fetch Data on Page Load

import { useState, useEffect } from "react";

function PostList() {
  const [posts, setPosts] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetch("https://jsonplaceholder.typicode.com/posts")
      .then(res => res.json())
      .then(data => {
        setPosts(data);
        setLoading(false);
      });
  }, []);

  if (loading) return <p>Loading posts...</p>;

  return (
    <ul>
      {posts.slice(0, 5).map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}
Enter fullscreen mode Exit fullscreen mode

What happens step by step:

Component loads
  → loading is true → "Loading posts..." shows
  → useEffect fires once
  → fetch gets the data
  → setPosts saves the data
  → setLoading sets to false
  → Component re-renders
  → Posts show on screen
Enter fullscreen mode Exit fullscreen mode

Empty array. Fetch runs once. That is all you need.


7. Example 2 — Timer with Cleanup

import { useState, useEffect } from "react";

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

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

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

  return <p>Time: {seconds} seconds</p>;
}
Enter fullscreen mode Exit fullscreen mode

Two parts inside this useEffect.

Setup — starts a timer that increases seconds every 1 second.

Cleanup — the return () => {} part. This runs when the component is removed from the screen.

Why do you need cleanup?

If the component is removed but the timer keeps running — it will try to update state on a component that no longer exists. That causes memory leaks and errors.

Think of it like this. You turn a fan on when you enter a room. When you leave — you turn it off. You do not leave it running.

The cleanup function is you turning the fan off.

Quick question for you.

What happens if you remove the return () => clearInterval(interval) line?

The timer keeps running even after the component is gone. Every second it tries to update state on something that no longer exists. That is a memory leak.

Always clean up what you set up.


8. One Common Mistake

Missing a value in the dependency array.

// Wrong — count is used inside but missing from array
useEffect(() => {
  console.log(count);
}, []);

// Right
useEffect(() => {
  console.log(count);
}, [count]);
Enter fullscreen mode Exit fullscreen mode

If you use a value inside useEffect — it must be in the dependency array.

If you forget it — the effect uses the old stale value and gives you wrong results.


Quick Summary — 4 Things to Remember

  1. useEffect runs code at specific times — not on every render. The dependency array controls when.

  2. Empty array [] — runs once when the component mounts. Use this for fetching data on load.

  3. Array with a value [value] — runs on mount and every time that value changes.

  4. Cleanup function — return a function from useEffect to stop timers or subscriptions when the component unmounts. Always clean up what you set up.


useEffect feels confusing at first because it is about timing — when does this code run and why.

But once that clicks — you will see it used everywhere. Data fetching. Timers. DOM updates. All of it lives inside useEffect.

Try the examples above. Change the dependency array. Add a console.log inside. Watch when it fires. That hands-on time builds real understanding.

If something did not click — drop a comment below. Happy to explain it differently.


Thanks for reading. Keep building.

Top comments (0)