Chapter 11: useEffect Hook
Once you truly understand useEffect, you’ll be able to handle almost any side effect in your app: fetching data, subscriptions, timers, DOM manipulations, and more.
We’ll go very slowly and clearly, like I’m sitting right next to you in Mumbai explaining it live with lots of examples you can copy-paste right now.
1. What is useEffect?
useEffect is a hook that lets you perform side effects in functional components.
Side effects = anything that reaches outside of React’s render flow:
- Fetching data from an API
- Setting up timers (setTimeout, setInterval)
- Adding event listeners (window resize, scroll, etc.)
- Manually changing the DOM
- Subscribing to services (WebSockets, Firebase, etc.)
Syntax:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 |
useEffect(() => { // Your side effect code here return () => { // Cleanup function (optional) }; }, [dependencies]); // ← dependency array (very important!) |
Key points:
- useEffect runs after every render (by default)
- You control when it runs using the dependency array
- The cleanup function runs before the next effect and when the component unmounts
2. Fetching Data with useEffect (Most Common Use Case)
Let’s build a realistic user profile that fetches data from an API.
Create src/components/UserProfile.tsx
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
import { useState, useEffect } from 'react'; interface User { id: number; name: string; email: string; city: string; } function UserProfile() { const [user, setUser] = useState<User | null>(null); const [loading, setLoading] = useState(true); const [error, setError] = useState<string | null>(null); useEffect(() => { // This runs once when component mounts (because empty dependency array []) const fetchUser = async () => { try { setLoading(true); setError(null); // Using JSONPlaceholder fake API const response = await fetch('https://jsonplaceholder.typicode.com/users/1'); if (!response.ok) throw new Error('Failed to fetch user'); const data = await response.json(); setUser(data); } catch (err) { setError(err instanceof Error ? err.message : 'Something went wrong'); } finally { setLoading(false); } }; fetchUser(); // Optional: Cleanup function (runs when component unmounts or before next effect) return () => { console.log('Cleaning up fetch effect...'); // Here you could cancel fetch if using AbortController }; }, []); // ← Empty array = run only once on mount if (loading) return <div style={{ padding: '40px', textAlign: 'center', fontSize: '24px' }}>Loading user... ⏳</div>; if (error) return <div style={{ padding: '40px', color: '#ff6b6b', textAlign: 'center' }}>Error: {error} 😔</div>; return ( <div style={{ padding: '40px', maxWidth: '600px', margin: '0 auto', background: '#f8f9ff', borderRadius: '16px', boxShadow: '0 8px 30px rgba(0,0,0,0.1)' }}> <h2 style={{ color: '#646cff' }}>User Profile</h2> <p><strong>Name:</strong> {user?.name}</p> <p><strong>Email:</strong> {user?.email}</p> <p><strong>City:</strong> {user?.address?.city}</p> <p style={{ marginTop: '30px', color: '#4ecdc4', fontWeight: 'bold' }}> Data fetched successfully! 🎉 </p> </div> ); } export default UserProfile; |
Important notes:
- We use empty dependency array [] → effect runs only once when component mounts
- We set loading and error states → great UX
- Cleanup function is optional here (but good practice)
3. Dependencies Array – Controlling When Effect Runs
The dependency array tells React when to re-run the effect.
| Dependency Array | When does effect run? |
|---|---|
| [] (empty) | Only once – on mount |
| No array at all | Every render (almost never what you want – causes infinite loops!) |
| [someValue] | On mount + whenever someValue changes |
| [userId] | Whenever userId changes → perfect for fetching user by ID |
Example: Fetch user when ID changes
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
function UserById({ userId }: { userId: number }) { const [user, setUser] = useState<User | null>(null); useEffect(() => { const fetchUser = async () => { const res = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`); const data = await res.json(); setUser(data); }; fetchUser(); }, [userId]); // ← Re-run whenever userId changes return user ? <h2>{user.name}</h2> : <p>Loading...</p>; } |
4. Cleanup Functions – Preventing Memory Leaks
Cleanup is super important for:
- Canceling timers
- Removing event listeners
- Aborting fetch requests
- Closing WebSocket connections
Example: Timer that updates every second
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
function LiveClock() { const [time, setTime] = useState(new Date().toLocaleTimeString()); useEffect(() => { const intervalId = setInterval(() => { setTime(new Date().toLocaleTimeString()); }, 1000); // Cleanup: clear interval when component unmounts return () => { clearInterval(intervalId); console.log('Timer cleaned up!'); }; }, []); // Only once on mount return ( <div style={{ padding: '40px', textAlign: 'center', fontSize: '48px', color: '#646cff' }}> {time} </div> ); } |
Without cleanup → timer keeps running even after component unmounts → memory leak!
5. Common Patterns & Best Practices (2026)
- Fetch data on mount → useEffect(…, [])
- Fetch when prop/id changes → useEffect(…, [id])
- Run on every render → avoid (remove dependency array) → causes infinite loops
- Cleanup always when you set up something (timers, listeners, subscriptions)
- Avoid putting state setters in dependencies unless needed (causes unnecessary re-runs)
Summary – Chapter 11 Key Takeaways
- useEffect = handle side effects (API calls, timers, listeners…)
- Runs after render
- Dependency array controls when it runs:
- [] → once on mount
- [dep] → on mount + when dep changes
- no array → every render (dangerous!)
- Cleanup function runs before next effect and on unmount
- Always handle loading, error, and success states
- Modern apps use React Query / SWR for data fetching (we’ll cover later)
Mini Homework
- Create a SearchUsers component
- Have an input field for user ID
- Use useEffect to fetch user when ID changes
- Show loading, error, and user data
- Bonus: Add a cleanup with AbortController to cancel fetch if user types fast
