useEffect & Side Effects
Side effects are operations that reach outside a component: API calls, timers, DOM manipulation, logging, etc. TheuseEffect hook is how React handles these operations in functional components.
What is a Side Effect?
Copy
Pure Component (no side effects):
┌─────────────────────────────────────────────┐
│ Props + State ──────► JSX Output │
│ │
│ Same inputs = Same output (predictable) │
└─────────────────────────────────────────────┘
Component with Side Effects:
┌─────────────────────────────────────────────┐
│ Props + State ──────► JSX Output │
│ │ │
│ ┌────────▼────────┐ │
│ │ Side Effects │ │
│ │ • API calls │ │
│ │ • Timers │ │
│ │ • DOM updates │ │
│ │ • Subscriptions│ │
│ └─────────────────┘ │
└─────────────────────────────────────────────┘
Basic useEffect Syntax
Copy
import { useEffect } from 'react';
useEffect(() => {
// Effect code runs here
return () => {
// Optional cleanup function
};
}, [dependencies]); // Dependency array
The Dependency Array
The dependency array controls when your effect runs:1. No Dependency Array — Runs After Every Render
Copy
useEffect(() => {
console.log('Runs after EVERY render');
});
This is rarely what you want! It can cause performance issues and infinite loops.
2. Empty Array — Runs Once on Mount
Copy
useEffect(() => {
console.log('Runs only on mount (like componentDidMount)');
}, []);
3. With Dependencies — Runs When Dependencies Change
Copy
useEffect(() => {
console.log(`Count changed to: ${count}`);
}, [count]); // Runs on mount AND when count changes
Visual Guide
Copy
function Counter() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// ❌ Runs on EVERY render
useEffect(() => {
document.title = `Count: ${count}`;
});
// ✅ Runs once on mount
useEffect(() => {
fetchInitialData();
}, []);
// ✅ Runs when count changes
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
// ✅ Runs when count OR name changes
useEffect(() => {
console.log({ count, name });
}, [count, name]);
}
Cleanup Functions
Some effects need cleanup: unsubscribing, clearing timers, removing listeners.Copy
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
// Effect: Start a timer
const intervalId = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// Cleanup: Clear the timer
return () => {
clearInterval(intervalId);
};
}, []); // Only run on mount/unmount
return <p>Seconds: {seconds}</p>;
}
When Cleanup Runs
Copy
Component mounts:
└── Effect runs
Dependency changes:
├── Previous cleanup runs
└── Effect runs again
Component unmounts:
└── Cleanup runs
Event Listener Example
Copy
function WindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
// Cleanup
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return <p>Window: {size.width} x {size.height}</p>;
}
Data Fetching
One of the most common use cases foruseEffect.
Basic Fetch
Copy
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchUser = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error('Failed to fetch user');
}
const data = await response.json();
setUser(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]); // Re-fetch when userId changes
if (loading) return <LoadingSpinner />;
if (error) return <ErrorMessage message={error} />;
if (!user) return null;
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
</div>
);
}
Handling Race Conditions
When fetching data based on changing props, you can have race conditions:Copy
function SearchResults({ query }) {
const [results, setResults] = useState([]);
useEffect(() => {
let isCancelled = false;
const search = async () => {
const data = await fetch(`/api/search?q=${query}`);
const json = await data.json();
// Only update if this effect wasn't cancelled
if (!isCancelled) {
setResults(json);
}
};
search();
// Cleanup: cancel if query changes before response
return () => {
isCancelled = true;
};
}, [query]);
return (
<ul>
{results.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
);
}
Using AbortController
The proper way to cancel fetch requests:Copy
function SearchResults({ query }) {
const [results, setResults] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
const controller = new AbortController();
const search = async () => {
try {
setLoading(true);
const response = await fetch(`/api/search?q=${query}`, {
signal: controller.signal
});
const data = await response.json();
setResults(data);
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Search failed:', error);
}
} finally {
setLoading(false);
}
};
if (query) {
search();
}
return () => {
controller.abort();
};
}, [query]);
return (/* ... */);
}
Common Patterns
1. Sync with External System
Copy
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
}
2. Update Document Title
Copy
function Page({ title }) {
useEffect(() => {
const originalTitle = document.title;
document.title = title;
return () => {
document.title = originalTitle;
};
}, [title]);
}
3. Local Storage Sync
Copy
function useLocalStorage(key, initialValue) {
const [value, setValue] = useState(() => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : initialValue;
});
useEffect(() => {
localStorage.setItem(key, JSON.stringify(value));
}, [key, value]);
return [value, setValue];
}
// Usage
function App() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
}
4. Debounced API Call
Copy
function SearchInput() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
useEffect(() => {
if (!query.trim()) {
setResults([]);
return;
}
const timeoutId = setTimeout(async () => {
const data = await fetchSearch(query);
setResults(data);
}, 300);
return () => clearTimeout(timeoutId);
}, [query]);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<ul>
{results.map(r => <li key={r.id}>{r.name}</li>)}
</ul>
</div>
);
}
5. Intersection Observer (Infinite Scroll)
Copy
function InfiniteList() {
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
const loaderRef = useRef(null);
useEffect(() => {
const fetchItems = async () => {
const data = await fetch(`/api/items?page=${page}`);
const newItems = await data.json();
setItems(prev => [...prev, ...newItems]);
};
fetchItems();
}, [page]);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
setPage(p => p + 1);
}
},
{ threshold: 1.0 }
);
if (loaderRef.current) {
observer.observe(loaderRef.current);
}
return () => observer.disconnect();
}, []);
return (
<div>
{items.map(item => <ItemCard key={item.id} item={item} />)}
<div ref={loaderRef}>Loading more...</div>
</div>
);
}
useEffect Pitfalls
1. Infinite Loops
Copy
// ❌ BAD - Object creates new reference each render
useEffect(() => {
setData({ count: count + 1 });
}, [data]); // data always "changes"
// ❌ BAD - Missing dependency
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // Stale closure!
}, 1000);
return () => clearInterval(id);
}, []); // count is missing
// ✅ FIXED - Use functional update
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
2. Stale Closures
Copy
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const handleClick = () => {
// ❌ This captures the initial count value
console.log('Count:', count);
};
document.addEventListener('click', handleClick);
return () => document.removeEventListener('click', handleClick);
}, []); // ❌ count not in dependencies
// ✅ FIXED: Add count to dependencies, or use a ref
}
3. Async Functions
Copy
// ❌ BAD - useEffect callback can't be async
useEffect(async () => {
const data = await fetch('/api/data');
}, []);
// ✅ GOOD - Define async function inside
useEffect(() => {
const fetchData = async () => {
const data = await fetch('/api/data');
};
fetchData();
}, []);
// ✅ ALSO GOOD - IIFE
useEffect(() => {
(async () => {
const data = await fetch('/api/data');
})();
}, []);
When NOT to Use useEffect
Some things don’t needuseEffect:
Transforming Data
Copy
// ❌ Don't use effect for derived data
const [items, setItems] = useState([]);
const [filteredItems, setFilteredItems] = useState([]);
useEffect(() => {
setFilteredItems(items.filter(i => i.active));
}, [items]);
// ✅ Just calculate it during render
const [items, setItems] = useState([]);
const filteredItems = items.filter(i => i.active);
Handling Events
Copy
// ❌ Don't use effect to respond to user actions
const [submitted, setSubmitted] = useState(false);
useEffect(() => {
if (submitted) {
sendToServer(formData);
}
}, [submitted]);
// ✅ Just handle in the event handler
const handleSubmit = () => {
sendToServer(formData);
};
🎯 Practice Exercises
Exercise 1: Fetch with Loading States
Exercise 1: Fetch with Loading States
Copy
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const controller = new AbortController();
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(url, { signal: controller.signal });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const json = await response.json();
setData(json);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err.message);
}
} finally {
setLoading(false);
}
};
fetchData();
return () => controller.abort();
}, [url]);
return { data, loading, error };
}
// Usage
function Users() {
const { data, loading, error } = useFetch('/api/users');
if (loading) return <Spinner />;
if (error) return <Error message={error} />;
return (
<ul>
{data.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
);
}
Exercise 2: Real-time Clock
Exercise 2: Real-time Clock
Copy
function Clock() {
const [time, setTime] = useState(new Date());
useEffect(() => {
const intervalId = setInterval(() => {
setTime(new Date());
}, 1000);
return () => clearInterval(intervalId);
}, []);
return (
<div className="clock">
<div className="time">
{time.toLocaleTimeString()}
</div>
<div className="date">
{time.toLocaleDateString(undefined, {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</div>
</div>
);
}
Exercise 3: Online/Offline Status
Exercise 3: Online/Offline Status
Copy
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
return isOnline;
}
function App() {
const isOnline = useOnlineStatus();
return (
<div>
<StatusBadge online={isOnline} />
{!isOnline && (
<div className="offline-banner">
You're offline. Some features may not work.
</div>
)}
</div>
);
}
Summary
| Concept | Description |
|---|---|
| Side Effect | Operation outside React’s render flow |
| useEffect | Hook for managing side effects |
| No dependencies | Runs after every render (rarely wanted) |
Empty [] | Runs once on mount |
[dep1, dep2] | Runs when dependencies change |
| Cleanup function | Returned function for cleanup |
| Race conditions | Handle with flags or AbortController |
| Stale closures | Use functional updates or add dependencies |
| Derived data | Calculate during render, not in effects |
Next Steps
In the next chapter, you’ll learn about the Context API — managing global state without prop drilling!