When you start with React, useState feels simple — it stores values and re-renders your component.
But then comes useEffect… and suddenly, beginners ask:
- “Why is my code running twice?”
- “Why do I need cleanup?”
- “What’s this dependency array thing?”
Let’s break it down step by step with real-life analogies!
🏠 What is a Side Effect?
In programming, a side effect is something that happens outside the main flow of your code.
👉 Think of it like this:
You enter your room → the light automatically turns on.
You leave the room → the light turns off.
The room is your component.
The light is a side effect.
It happens because of your presence, but it’s not the main job of being in the room.
In React, these side effects could be:
- Fetching data from an API
- Setting up event listeners
- Starting/stopping a timer
- Updating the document title
⚡ Meet useEffect
Here’s the syntax:
useEffect(() => { // side effect code here return () => { // cleanup code here (optional) }; }, [dependencies]);
- The first function is what runs as the side effect.
- The return function is the cleanup (think of it as “turning off the light when you leave”).
- The [dependencies] array tells React when to run the effect.
📦 Real-Life Example: Online Shopping
You add a product to your cart → instantly, you get an order confirmation email.
The cart = State
The email = Side effect
import { useState, useEffect } from "react"; function Cart() { const [items, setItems] = useState([]); useEffect(() => { console.log("📧 Sending confirmation email…"); }, [items]); // runs whenever items change return ( <div> <button onClick={() => setItems([...items, "Shoe"])}>Add Shoe</button> <p>Items: {items.join(", ")}</p> </div> ); }
🔄 Dependency Array in useEffect
🧃 Example 1: No Dependency Array → Runs on Every Render
Imagine you open a fridge — every time you even peek inside, the fridge alarm beeps. Annoying, right?
useEffect(() => { console.log("Effect ran!"); });
📌 This runs every single render, which can cause unnecessary work.
📦 Example 2: Empty Array [] → Runs Only Once
Like a fridge alarm that only beeps once when you first open it.
useEffect(() => { console.log("Effect ran only once (on mount)."); }, []);
📞 Example 3: With Dependencies [count] → Runs When Count Changes
Think of it like a reminder that only triggers when a specific condition changes.
useEffect(() => { console.log("Effect ran because count changed:", count); }, [count]);
📌 Effect re-runs only when the count changes.
🧹 Why Cleanup Functions Matter
Cleanup = “turning things off” before setting them up again.
Without cleanup, you can end up with duplicate event listeners, memory leaks, or multiple timers running at once.
🎵 Example 4: Timer Without Cleanup (Buggy)
useEffect(() => { const interval = setInterval(() => { console.log("⏰ Timer tick..."); }, 1000); }, [count]);
Every time the count changes, a new interval starts.
Old intervals are never cleared → multiple timers run together → app lags.
✅ Example 5: Timer With Cleanup (Correct)
useEffect(() => { const interval = setInterval(() => { console.log("⏰ Timer tick..."); }, 1000); return () => { clearInterval(interval); // cleanup old timer console.log("🧹 Timer cleaned up"); }; }, [count]);
📌 Now, whenever the count changes:
Old timer is cleared. The new timer starts fresh.
Just like turning off the old alarm before setting a new one.
Top comments (1)
Nicely explained 🙌