The useEffect Hook: Your Componentβs Helpful Robot Assistant π€
Imagine you have a robot helper in your room. This robot watches what you do and responds to help you. When you wake up, it opens the curtains. When you leave, it turns off the lights. When you come back with new toys, it rearranges the shelf.
Thatβs exactly what useEffect does for your React components!
What is useEffect?
Think of your React component as a living room. The component renders (like decorating the room), and sometimes you need side effects β things that happen outside the room but are triggered by changes inside it.
Side effects include:
- π‘ Fetching data from the internet
- β° Setting up timers
- π Updating the browserβs title
- π Connecting to external services
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
// This is your robot's task!
document.title = "Hello!";
});
return <h1>Welcome</h1>;
}
The robot runs its task after your room is decorated (after render).
useEffect Basics
The simplest useEffect runs after every render:
useEffect(() => {
console.log("Component rendered!");
});
Think of it like this:
- π¨ React paints your component
- π€ Robot wakes up and does its job
- β Done!
The Three Parts of useEffect
useEffect(() => {
// 1. SETUP: What to do
const timer = setInterval(tick, 1000);
// 2. CLEANUP: How to undo it
return () => clearInterval(timer);
}, [dependencies]); // 3. WHEN to react
Effect Dependencies
The dependency array tells your robot: βOnly work when these things change!β
No Array = Run Every Time
useEffect(() => {
console.log("I run after EVERY render");
});
// Robot works constantly! π°
Empty Array = Run Once
useEffect(() => {
console.log("I run ONCE when born");
}, []);
// Robot works only on first day! π
With Dependencies = Run When They Change
useEffect(() => {
console.log(`Count is now ${count}`);
}, [count]);
// Robot works only when count changes! π―
Real Example:
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
// Fetch new user when userId changes
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data));
}, [userId]); // π Only re-run when userId changes
return <div>{user?.name}</div>;
}
Effect Cleanup
Your robot is polite! Before leaving or doing a new task, it cleans up the old mess.
Why Cleanup?
Imagine you start a timer. If you leave without stopping it, it keeps ticking forever β wasting energy and causing bugs!
useEffect(() => {
// SETUP: Start a subscription
const subscription = api.subscribe(userId);
// CLEANUP: Cancel when leaving
return () => {
subscription.unsubscribe();
};
}, [userId]);
When Does Cleanup Run?
βββββββββββββββββββββββββββββββββββ
β 1. Component mounts β
β β Effect runs (setup) β
βββββββββββββββββββββββββββββββββββ€
β 2. Dependency changes β
β β Cleanup runs (old effect) β
β β Effect runs (new setup) β
βββββββββββββββββββββββββββββββββββ€
β 3. Component unmounts β
β β Cleanup runs (final) β
βββββββββββββββββββββββββββββββββββ
Timer Example:
function Clock() {
const [time, setTime] = useState(new Date());
useEffect(() => {
const id = setInterval(() => {
setTime(new Date());
}, 1000);
// Cleanup stops the timer!
return () => clearInterval(id);
}, []);
return <p>{time.toLocaleTimeString()}</p>;
}
Effect Remounting
In Reactβs Strict Mode (development), your component mounts twice on purpose. This helps catch bugs!
Normal: Mount β Effect runs
Strict: Mount β Effect β Unmount β Mount β Effect
Why? React wants to test if your cleanup works properly.
The Test
useEffect(() => {
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, []);
Good code: Connects β Disconnects β Connects (works!) Bad code: Connects β Nothing β Connects (two connections! π₯)
If your effect breaks with remounting, you have a cleanup bug!
When NOT to Use Effects
Stop! π Not everything needs useEffect. Many developers overuse it!
β DONβT: Transform Data for Rendering
// BAD β
const [filtered, setFiltered] = useState([]);
useEffect(() => {
setFiltered(items.filter(x => x.active));
}, [items]);
// GOOD β
(Calculate during render!)
const filtered = items.filter(x => x.active);
β DONβT: Handle User Events
// BAD β
useEffect(() => {
if (submitted) {
sendData();
}
}, [submitted]);
// GOOD β
(Handle in the event!)
function handleSubmit() {
sendData();
}
β DO Use Effects For:
- π Fetching data from APIs
- π Subscribing to external events
- β° Setting up timers
- π Analytics tracking
- π¨ Syncing with non-React widgets
Flow Chart
graph TD A[Need a side effect?] --> B{Is it during render?} B -->|Yes| C[Calculate inline - no effect!] B -->|No| D{Triggered by event?} D -->|Yes| E[Put in event handler!] D -->|No| F[Use useEffect β ]
Removing Dependencies
Sometimes your effect has too many dependencies. Hereβs how to trim them!
Problem: Updating State Based on Previous
// BAD: count is a dependency
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // π Needs count!
}, 1000);
return () => clearInterval(id);
}, [count]); // Restarts every second! π±
Solution: Use Functional Updates
// GOOD: No dependency on count!
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1); // π Uses previous value
}, 1000);
return () => clearInterval(id);
}, []); // Runs once! π
Moving Functions Inside Effects
// BAD: fetchData changes every render
function fetchData() {
fetch(`/api/${userId}`);
}
useEffect(() => {
fetchData();
}, [fetchData]); // Runs too often!
// GOOD: Define inside effect
useEffect(() => {
function fetchData() {
fetch(`/api/${userId}`);
}
fetchData();
}, [userId]); // Only runs when userId changes!
Stale Closures
A stale closure is when your effect remembers old values instead of current ones. Itβs like your robot using an outdated shopping list!
The Problem
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
console.log(count); // Always logs 0! π±
}, 1000);
return () => clearInterval(id);
}, []); // π Empty array = captures initial count
return (
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
);
}
Why? The effect βcloses overβ the initial count (0) and never updates.
Solution 1: Add Dependency
useEffect(() => {
const id = setInterval(() => {
console.log(count); // β
Always current!
}, 1000);
return () => clearInterval(id);
}, [count]); // π Re-runs when count changes
Solution 2: Use Ref for Latest Value
function Counter() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
// Keep ref in sync
useEffect(() => {
countRef.current = count;
}, [count]);
useEffect(() => {
const id = setInterval(() => {
console.log(countRef.current); // β
Always current!
}, 1000);
return () => clearInterval(id);
}, []);
return <button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>;
}
Stale Closure Flow
graph TD A[Effect Created] --> B[Captures count=0] B --> C[Interval Runs] C --> D{count updated?} D -->|Yes| E[count=5 in state] D -->|No| C E --> F[But effect still sees 0!] F --> G[STALE CLOSURE! π]
Quick Reference Summary
| Concept | What It Means |
|---|---|
| Basics | Code that runs after render |
| Dependencies | Control when effect re-runs |
| Cleanup | Undo your effect before next run |
| Remounting | Strict Mode tests your cleanup |
| Not for effects | Calculations & event handlers |
| Removing deps | Use functional updates |
| Stale closures | Old values trapped in effects |
The Golden Rules π
- Effects are for syncing with external systems, not internal calculations
- Always clean up subscriptions, timers, and connections
- Keep dependencies honest β include what you use
- Use functional updates to reduce dependencies
- Watch for stale closures β your effect might be reading old data!
Your robot helper is powerful, but only if you give it clear instructions! π€β¨