React component functions are supposed to be pure โ given the same props and state, they should return the same JSX, with no observable side effects during rendering. But real applications cannot be purely functional: they need to fetch data from servers, update the browser tab title, set up WebSocket connections, and read from localStorage. These are side effects โ operations that interact with the world outside React’s rendering system. The useEffect hook is how React lets you run side effects safely, after rendering, in a controlled way. Understanding this โ what a side effect is, why it cannot run during render, and what useEffect provides โ is the foundation for every data-fetching, event-listening, and timer-based feature in your MERN Blog.
What Is a Side Effect?
| Not a Side Effect (safe in render) | Side Effect (needs useEffect) |
|---|---|
| Computing a filtered list from state | Fetching data from an API |
| Formatting a date string | Setting document.title |
| Returning JSX | Reading or writing to localStorage |
| Calling a pure utility function | Setting up a timer (setTimeout, setInterval) |
| Performing arithmetic | Adding/removing DOM event listeners |
| Deriving state from props | Opening a WebSocket connection |
useEffect solves this by deferring side effects until after the render is committed to the DOM.useEffect as a hook that says “after this render is done and painted to the screen, run this code.” This is distinct from rendering itself. Your component function describes what the UI should look like. useEffect describes what to do after the UI is shown โ fetch data, update the title, set up a listener.useEffect Syntax
import { useEffect } from 'react';
// Basic form
useEffect(
() => {
// Side effect code runs here โ after render
// ...
return () => {
// Optional cleanup function โ runs before the next effect or on unmount
};
},
[/* dependency array */]
);
The Three Dependency Array Forms
// 1. No dependency array โ runs after EVERY render
useEffect(() => {
console.log('Rendered!'); // runs on every render โ rarely what you want
});
// 2. Empty array โ runs ONCE after the initial render (mount)
useEffect(() => {
console.log('Component mounted!'); // runs once โ fetch initial data here
}, []);
// 3. Array with values โ runs when those values change
useEffect(() => {
console.log(`Page changed to: ${page}`); // runs on mount and when page changes
}, [page]);
The React Component Lifecycle โ with useEffect
Component Lifecycle (Function Component)
Mount:
1. Component function runs โ JSX returned โ DOM updated
2. useEffect(fn, []) runs โ initial data fetch, event listeners, etc.
Update (state or prop changes):
1. Component function runs again โ new JSX โ DOM updated
2. useEffect cleanup runs for effects whose dependencies changed
3. useEffect(fn, [dep]) runs again if dep changed
Unmount (component removed from tree):
1. useEffect cleanup functions run โ cancel requests, clear timers, remove listeners
2. Component is removed from DOM
Simple useEffect Examples
// โโ Update document title โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function PostPage({ post }) {
useEffect(() => {
if (post) {
document.title = `${post.title} | MERN Blog`; // side effect
}
return () => {
document.title = 'MERN Blog'; // cleanup: reset on unmount
};
}, [post]); // re-run when post changes
return <article><h1>{post?.title}</h1></article>;
}
// โโ Simple mount-only effect โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function Analytics() {
useEffect(() => {
// Track page view โ runs once on mount
analytics.track('page_view', { page: window.location.pathname });
}, []); // empty deps โ runs once only
return null;
}
// โโ Sync state to localStorage โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function ThemeToggle() {
const [theme, setTheme] = useState(() => localStorage.getItem('theme') || 'light');
useEffect(() => {
localStorage.setItem('theme', theme);
document.documentElement.setAttribute('data-theme', theme);
}, [theme]); // sync whenever theme changes
return (
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
{theme === 'light' ? '๐ Dark' : 'โ๏ธ Light'}
</button>
);
}
Common Mistakes
Mistake 1 โ Running a side effect directly in the component body
โ Wrong โ API call during render causes infinite loops or duplicated requests:
function PostList() {
const [posts, setPosts] = useState([]);
// This runs on EVERY render โ including the render caused by setPosts!
fetch('/api/posts').then(r => r.json()).then(d => setPosts(d.data));
return <div>...</div>;
}
โ Correct โ put the fetch inside useEffect:
function PostList() {
const [posts, setPosts] = useState([]);
useEffect(() => {
fetch('/api/posts').then(r => r.json()).then(d => setPosts(d.data));
}, []); // โ runs once after mount
return <div>...</div>;
}
Mistake 2 โ Using async directly as the useEffect callback
โ Wrong โ useEffect callback cannot be async:
useEffect(async () => { // โ async function returns a Promise, not a cleanup function
const data = await fetch('/api/posts');
}, []);
โ Correct โ define an async function inside and call it:
useEffect(() => {
const fetchPosts = async () => {
const res = await fetch('/api/posts');
const data = await res.json();
setPosts(data.data);
};
fetchPosts(); // call the async function
}, []); // โ outer callback is synchronous
Mistake 3 โ Forgetting that StrictMode double-invokes effects
โ Wrong โ effect creates a resource but cleanup does not release it:
useEffect(() => {
const ws = new WebSocket('wss://api.example.com');
// No cleanup โ StrictMode creates two WebSocket connections in development!
}, []);
โ Correct โ always clean up resources the effect creates:
useEffect(() => {
const ws = new WebSocket('wss://api.example.com');
return () => ws.close(); // โ cleanup closes the connection
}, []);
Quick Reference
| Form | When It Runs | Use For |
|---|---|---|
useEffect(fn) |
After every render | Rarely โ logging, debugging |
useEffect(fn, []) |
Once after mount | Initial data fetch, event listeners |
useEffect(fn, [x]) |
After mount + when x changes | Refetch when ID changes, sync to storage |
| Return cleanup function | Before next effect + on unmount | Cancel requests, clear timers, remove listeners |