React useEffect Hook - Complete Guide
The useEffect hook is one of the most powerful and frequently used hooks in React. It lets you perform side effects in functional components, such as data fetching, subscriptions, timers, and manually changing the DOM. This guide covers everything you need to know about useEffect.
What is useEffect?
useEffect is a hook that lets you run side effects in your components. Side effects are operations that affect something outside the component scope, like:
- Fetching data from APIs
- Setting up subscriptions
- Manually changing the DOM
- Setting timers (setTimeout, setInterval)
- Logging to the console
Basic useEffect Syntax
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
// This code runs after the component renders
console.log('Component rendered');
});
return <div>Hello World</div>;
}useEffect with Dependencies
The dependency array controls when the effect runs:
1. No Dependencies (Runs on every render)
function EveryRenderEffect() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('Effect runs on every render');
document.title = `Count: ${count}`;
});
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}2. Empty Dependency Array (Runs once on mount)
function OnceEffect() {
useEffect(() => {
console.log('Component mounted');
// This runs only once when the component mounts
const timer = setTimeout(() => {
console.log('Timer fired after 2 seconds');
}, 2000);
// Cleanup function
return () => {
clearTimeout(timer);
console.log('Component unmounted');
};
}, []); // Empty dependency array
return <div>Check the console</div>;
}3. With Dependencies (Runs when dependencies change)
function DependencyEffect() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
useEffect(() => {
console.log('Count changed:', count);
document.title = `Count: ${count}`;
}, [count]); // Only runs when count changes
useEffect(() => {
console.log('Name changed:', name);
}, [name]); // Only runs when name changes
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter your name"
/>
</div>
);
}Cleanup Functions
Effects can return cleanup functions that run when the component unmounts or before the effect re-runs:
function TimerComponent() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
console.log('Setting up timer');
const intervalId = setInterval(() => {
setSeconds(prev => prev + 1);
}, 1000);
// Cleanup function
return () => {
console.log('Cleaning up timer');
clearInterval(intervalId);
};
}, []); // Empty array means this effect runs only once
return <div>Timer: {seconds} seconds</div>;
}Common useEffect Patterns
1. Data Fetching
function DataFetcher({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchUser = async () => {
try {
setLoading(true);
const response = await fetch(`https://api.example.com/users/${userId}`);
if (!response.ok) {
throw new Error('Failed to fetch user');
}
const userData = await response.json();
setUser(userData);
setError(null);
} catch (err) {
setError(err.message);
setUser(null);
} finally {
setLoading(false);
}
};
if (userId) {
fetchUser();
}
}, [userId]); // Re-fetch when userId changes
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>No user found</div>;
return (
<div>
<h2>{user.name}</h2>
<p>Email: {user.email}</p>
</div>
);
}2. Event Listeners
function WindowSizeTracker() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
// Cleanup
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // Set up listener only once
return (
<div>
<p>Window width: {windowSize.width}px</p>
<p>Window height: {windowSize.height}px</p>
</div>
);
}3. Local Storage
function PersistentCounter() {
const [count, setCount] = useState(() => {
// Initialize from localStorage
const savedCount = localStorage.getItem('count');
return savedCount ? parseInt(savedCount, 10) : 0;
});
useEffect(() => {
// Save to localStorage whenever count changes
localStorage.setItem('count', count.toString());
}, [count]);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
<button onClick={() => setCount(0)}>Reset</button>
</div>
);
}4. WebSocket Connection
function WebSocketComponent({ url }) {
const [messages, setMessages] = useState([]);
const [connectionStatus, setConnectionStatus] = useState('disconnected');
useEffect(() => {
if (!url) return;
const ws = new WebSocket(url);
ws.onopen = () => {
setConnectionStatus('connected');
console.log('WebSocket connected');
};
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
setMessages(prev => [...prev, message]);
};
ws.onclose = () => {
setConnectionStatus('disconnected');
console.log('WebSocket disconnected');
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
setConnectionStatus('error');
};
// Cleanup
return () => {
if (ws.readyState === WebSocket.OPEN) {
ws.close();
}
};
}, [url]);
return (
<div>
<p>Status: {connectionStatus}</p>
<div>
{messages.map((message, index) => (
<div key={index}>{JSON.stringify(message)}</div>
))}
</div>
</div>
);
}Advanced useEffect Patterns
1. Custom Hook for API Calls
function useApi(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let isCancelled = false;
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const result = await response.json();
if (!isCancelled) {
setData(result);
setError(null);
}
} catch (err) {
if (!isCancelled) {
setError(err.message);
setData(null);
}
} finally {
if (!isCancelled) {
setLoading(false);
}
}
};
if (url) {
fetchData();
}
return () => {
isCancelled = true;
};
}, [url]);
return { data, loading, error };
}
// Usage
function UserProfile({ userId }) {
const { data: user, loading, error } = useApi(
userId ? `https://api.example.com/users/${userId}` : null
);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
if (!user) return <div>No user data</div>;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}2. Debounced Search
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
function SearchComponent() {
const [searchTerm, setSearchTerm] = useState('');
const [results, setResults] = useState([]);
const debouncedSearchTerm = useDebounce(searchTerm, 500);
useEffect(() => {
if (debouncedSearchTerm) {
// Perform search with debounced term
fetch(`https://api.example.com/search?q=${debouncedSearchTerm}`)
.then(response => response.json())
.then(data => setResults(data));
} else {
setResults([]);
}
}, [debouncedSearchTerm]);
return (
<div>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
<div>
{results.map((result, index) => (
<div key={index}>{result.name}</div>
))}
</div>
</div>
);
}3. Previous Value Hook
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
function CounterWithPrevious() {
const [count, setCount] = useState(0);
const previousCount = usePrevious(count);
return (
<div>
<p>Current count: {count}</p>
<p>Previous count: {previousCount}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}Common useEffect Mistakes and Solutions
1. Missing Dependencies
// Bad - missing dependencies
function BadExample({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`https://api.example.com/users/${userId}`)
.then(response => response.json())
.then(data => setUser(data));
}); // Missing userId dependency
return <div>{user?.name}</div>;
}
// Good - include all dependencies
function GoodExample({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`https://api.example.com/users/${userId}`)
.then(response => response.json())
.then(data => setUser(data));
}, [userId]); // Include userId
return <div>{user?.name}</div>;
}2. Infinite Loops
// Bad - causes infinite loop
function BadLoop() {
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // This triggers the effect again
}, [count]);
return <div>Count: {count}</div>;
}
// Good - proper dependency management
function GoodLoop() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(prev => prev + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>Increment</button>
</div>
);
}3. Async Functions in useEffect
// Bad - async function directly in useEffect
function BadAsync() {
const [data, setData] = useState(null);
useEffect(async () => {
const response = await fetch('https://api.example.com/data');
const result = await response.json();
setData(result);
}, []);
return <div>{JSON.stringify(data)}</div>;
}
// Good - async function inside useEffect
function GoodAsync() {
const [data, setData] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('https://api.example.com/data');
const result = await response.json();
setData(result);
} catch (error) {
console.error('Error fetching data:', error);
}
};
fetchData();
}, []);
return <div>{JSON.stringify(data)}</div>;
}Performance Optimization
1. Memoizing Callbacks
function OptimizedComponent({ userId }) {
const [user, setUser] = useState(null);
// Memoize the callback to prevent unnecessary re-renders
const handleUserUpdate = useCallback((updatedUser) => {
setUser(updatedUser);
}, []);
useEffect(() => {
// Effect will only re-run if userId or handleUserUpdate changes
const fetchUser = async () => {
const response = await fetch(`https://api.example.com/users/${userId}`);
const userData = await response.json();
handleUserUpdate(userData);
};
fetchUser();
}, [userId, handleUserUpdate]);
return <div>{user?.name}</div>;
}2. Conditional Effects
function ConditionalEffect({ shouldFetch, userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
// Only run effect if conditions are met
if (!shouldFetch || !userId) {
return;
}
let isCancelled = false;
const fetchUser = async () => {
try {
const response = await fetch(`https://api.example.com/users/${userId}`);
const userData = await response.json();
if (!isCancelled) {
setUser(userData);
}
} catch (error) {
if (!isCancelled) {
console.error('Error fetching user:', error);
}
}
};
fetchUser();
return () => {
isCancelled = true;
};
}, [shouldFetch, userId]);
return <div>{user?.name}</div>;
}useEffect vs useLayoutEffect
function EffectComparison() {
const [content, setContent] = useState('Initial content');
// useEffect - Runs after paint (non-blocking)
useEffect(() => {
console.log('useEffect ran');
// Good for data fetching, subscriptions, etc.
}, [content]);
// useLayoutEffect - Runs before paint (blocking)
useLayoutEffect(() => {
console.log('useLayoutEffect ran');
// Good for DOM measurements, layout calculations
const height = document.getElementById('content').offsetHeight;
console.log('Content height:', height);
}, [content]);
return (
<div>
<button onClick={() => setContent('Updated content')}>
Update Content
</button>
<div id="content">{content}</div>
</div>
);
}Summary
The useEffect hook is essential for handling side effects in React components. Remember these key points:
- Use empty dependency array
[]for effects that run once on mount - Include all variables used in the effect in the dependency array
- Return cleanup functions to prevent memory leaks
- Handle async operations properly inside effects
- Use custom hooks to reuse effect logic
- Be careful with dependencies to avoid infinite loops
- Consider performance with useCallback and useMemo
- Use useLayoutEffect for DOM measurements that need to happen before paint
Mastering useEffect will enable you to build sophisticated React applications that can handle real-world scenarios like data fetching, subscriptions, and DOM manipulation.
External Resources:
Related Tutorials: