useEffect Hook
useEffect - Side Effects in React
The useEffect hook lets you perform side effects in function components. Side effects are operations that interact with the outside world: data fetching, subscriptions, timers, manual DOM manipulation, and more.
What is useEffect?
import { useEffect } from 'react';
function Component() {
useEffect(() => {
// Side effect code here
console.log('Effect ran!');
});
return <div>Hello</div>;
}
useEffect runs after every render by default.
Basic Syntax
useEffect(() => {
// Effect code
return () => {
// Cleanup code (optional)
};
}, [dependencies]);
Three parts:
- Effect function - runs after render
- Cleanup function - runs before effect re-runs or component unmounts (optional)
- Dependency array - controls when effect runs (optional)
Dependency Array
Controls when the effect runs:
No dependency array - runs after every render:
useEffect(() => {
console.log('Runs after EVERY render');
});
Empty array - runs once on mount:
useEffect(() => {
console.log('Runs ONCE after first render');
}, []);
With dependencies - runs when dependencies change:
useEffect(() => {
console.log('Runs when count changes');
}, [count]);
useEffect(() => {
console.log('Runs when count OR name changes');
}, [count, name]);
Common Use Cases
1. Data Fetching
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetch(`/api/users/${userId}`)
.then(response => response.json())
.then(data => {
setUser(data);
setLoading(false);
});
}, [userId]); // Re-fetch when userId changes
if (loading) return <div>Loading...</div>;
if (!user) return <div>User not found</div>;
return <div>{user.name}</div>;
}
2. Subscriptions
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
// Subscribe
const subscription = chatAPI.subscribe(roomId, (message) => {
setMessages(prev => [...prev, message]);
});
// Cleanup: unsubscribe when component unmounts or roomId changes
return () => {
subscription.unsubscribe();
};
}, [roomId]);
return (
<div>
{messages.map(msg => <div key={msg.id}>{msg.text}</div>)}
</div>
);
}
3. Document Title
function PageTitle({ title }) {
useEffect(() => {
document.title = title;
}, [title]);
return <h1>{title}</h1>;
}
4. Timers
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// Cleanup: clear interval on unmount
return () => clearInterval(interval);
}, []); // Empty array = set up once
return <div>Seconds: {seconds}</div>;
}
5. localStorage Sync
function Settings() {
const [theme, setTheme] = useState(() => {
return localStorage.getItem('theme') || 'light';
});
useEffect(() => {
localStorage.setItem('theme', theme);
}, [theme]);
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Current theme: {theme}
</button>
);
}
Cleanup Function
Return a function to clean up side effects:
useEffect(() => {
// Set up
const subscription = api.subscribe();
// Clean up
return () => {
subscription.unsubscribe();
};
}, []);
When cleanup runs:
- Before the effect runs again (when dependencies change)
- When component unmounts
Example with lifecycle:
function Component({ id }) {
useEffect(() => {
console.log('Effect: Setting up for', id);
return () => {
console.log('Cleanup: Cleaning up for', id);
};
}, [id]);
return <div>{id}</div>;
}
// Renders with id=1: "Effect: Setting up for 1"
// Re-renders with id=2: "Cleanup: Cleaning up for 1", then "Effect: Setting up for 2"
// Unmounts: "Cleanup: Cleaning up for 2"
Multiple useEffect Hooks
Separate concerns into different effects:
function UserDashboard({ userId }) {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
// Effect 1: Fetch user
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(setUser);
}, [userId]);
// Effect 2: Fetch posts
useEffect(() => {
fetch(`/api/users/${userId}/posts`)
.then(r => r.json())
.then(setPosts);
}, [userId]);
// Effect 3: Update page title
useEffect(() => {
if (user) {
document.title = `${user.name}'s Profile`;
}
}, [user]);
return <div>...</div>;
}
Async in useEffect
Cannot make useEffect callback async directly:
// ❌ Wrong - useEffect callback can't be async
useEffect(async () => {
const data = await fetchData();
}, []);
Define async function inside:
// ✅ Right
useEffect(() => {
async function fetchData() {
const response = await fetch('/api/data');
const data = await response.json();
setData(data);
}
fetchData();
}, []);
// ✅ Also right - IIFE
useEffect(() => {
(async () => {
const data = await fetchData();
setData(data);
})();
}, []);
Practical Examples
Example 1: Search with Debounce
function SearchBox() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
useEffect(() => {
if (!query) {
setResults([]);
return;
}
// Debounce: wait 500ms after user stops typing
const timer = setTimeout(() => {
fetch(`/api/search?q=${query}`)
.then(r => r.json())
.then(setResults);
}, 500);
// Cleanup: cancel previous timer if user types again
return () => clearTimeout(timer);
}, [query]);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
<ul>
{results.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}
Example 2: Window Event Listener
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: remove listener
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // Empty array - set up once
return <div>Window: {size.width} x {size.height}</div>;
}
Example 3: Fetch with Loading & Error
function UserList() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
setError(null);
fetch('/api/users')
.then(response => {
if (!response.ok) throw new Error('Failed to fetch');
return response.json();
})
.then(data => {
setUsers(data);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Example 4: Auto-save Form
function AutoSaveForm() {
const [formData, setFormData] = useState({ title: '', content: '' });
const [saveStatus, setSaveStatus] = useState('');
useEffect(() => {
// Don't save on initial render
if (formData.title === '' && formData.content === '') return;
setSaveStatus('Saving...');
const timer = setTimeout(() => {
// Simulate API call
localStorage.setItem('draft', JSON.stringify(formData));
setSaveStatus('Saved!');
// Clear status after 2 seconds
setTimeout(() => setSaveStatus(''), 2000);
}, 1000); // Wait 1 second after user stops typing
return () => clearTimeout(timer);
}, [formData]);
return (
<div>
<input
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
placeholder="Title"
/>
<textarea
value={formData.content}
onChange={(e) => setFormData({ ...formData, content: e.target.value })}
placeholder="Content"
/>
<div className="save-status">{saveStatus}</div>
</div>
);
}
Example 5: WebSocket Connection
function LiveFeed() {
const [messages, setMessages] = useState([]);
useEffect(() => {
const ws = new WebSocket('wss://api.example.com/live');
ws.onopen = () => {
console.log('Connected');
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
setMessages(prev => [message, ...prev].slice(0, 50)); // Keep last 50
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
// Cleanup: close connection
return () => {
ws.close();
};
}, []); // Connect once on mount
return (
<div>
{messages.map((msg, i) => (
<div key={i}>{msg.text}</div>
))}
</div>
);
}
Dependency Array Best Practices
1. Include all dependencies:
// ❌ Bad - missing dependency
useEffect(() => {
console.log(count);
}, []); // Should include count!
// ✅ Good
useEffect(() => {
console.log(count);
}, [count]);
2. Use ESLint plugin:
npm install eslint-plugin-react-hooks
This catches missing dependencies automatically.
3. Be careful with objects/arrays:
// ❌ Bad - new object every render, infinite loop!
const config = { apiUrl: '/api' };
useEffect(() => {
fetch(config.apiUrl);
}, [config]); // New object reference every time!
// ✅ Good - primitive value
const apiUrl = '/api';
useEffect(() => {
fetch(apiUrl);
}, [apiUrl]);
// ✅ Good - memoize object
const config = useMemo(() => ({ apiUrl: '/api' }), []);
useEffect(() => {
fetch(config.apiUrl);
}, [config]);
Common Patterns
1. Conditional effect:
useEffect(() => {
if (!shouldRun) return;
// Effect code
}, [shouldRun, otherDeps]);
2. Race condition prevention:
useEffect(() => {
let cancelled = false;
fetch('/api/data')
.then(r => r.json())
.then(data => {
if (!cancelled) { // Only update if still mounted
setData(data);
}
});
return () => {
cancelled = true; // Mark as cancelled on cleanup
};
}, []);
3. Combining with custom hooks:
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
setLoading(true);
fetch(url)
.then(r => r.json())
.then(data => {
setData(data);
setLoading(false);
});
}, [url]);
return { data, loading };
}
// Usage
function Component() {
const { data, loading } = useFetch('/api/users');
if (loading) return <div>Loading...</div>;
return <div>{data.name}</div>;
}
Best Practices
1. Keep effects focused:
// Bad - doing too much in one effect
useEffect(() => {
fetchUser();
updateTitle();
trackAnalytics();
subscribeToUpdates();
}, []);
// Good - separate concerns
useEffect(() => { fetchUser(); }, []);
useEffect(() => { updateTitle(); }, [user]);
useEffect(() => { trackAnalytics(); }, [page]);
useEffect(() => {
const sub = subscribeToUpdates();
return () => sub.unsubscribe();
}, []);
2. Always clean up:
// Good - cleanup timers
useEffect(() => {
const timer = setTimeout(() => {}, 1000);
return () => clearTimeout(timer);
}, []);
// Good - cleanup listeners
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
3. Don’t lie about dependencies:
// ❌ Bad - lying to ESLint
useEffect(() => {
doSomething(value);
}, []); // eslint-disable-line
// ✅ Good - honest dependencies
useEffect(() => {
doSomething(value);
}, [value]);
Common Mistakes
1. Infinite loop:
// ❌ Causes infinite loop
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // Updates count, triggers effect again!
});
// ✅ Fixed with dependency array
useEffect(() => {
setCount(count + 1);
}, []); // Runs once
2. Missing cleanup:
// ❌ Memory leak
useEffect(() => {
const interval = setInterval(() => {
console.log('tick');
}, 1000);
// No cleanup!
}, []);
// ✅ Fixed
useEffect(() => {
const interval = setInterval(() => {
console.log('tick');
}, 1000);
return () => clearInterval(interval);
}, []);
3. Async callback:
// ❌ Wrong
useEffect(async () => {
await fetch('/api/data');
}, []);
// ✅ Right
useEffect(() => {
fetch('/api/data');
}, []);
// ✅ Right with async
useEffect(() => {
async function loadData() {
await fetch('/api/data');
}
loadData();
}, []);
Next Article: Conditional Rendering