React useEffect Hook - Complete Guide and Common Mistakes
The useEffect hook is one of the most powerful and commonly misused hooks in React. This guide covers all useEffect patterns, common mistakes, and best practices for building reliable React applications.
Understanding useEffect
useEffect lets you perform side effects in functional components, replacing class component lifecycle methods like componentDidMount, componentDidUpdate, and componentWillUnmount.
Basic Syntax
import { useEffect } from 'react';
function MyComponent() {
useEffect(() => {
// Side effect code here
return () => {
// Cleanup code (optional)
};
}, []); // Dependency array
return <div>My Component</div>;
}
Common useEffect Patterns
1. Component Mount (Similar to componentDidMount)
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
// Runs once when component mounts
useEffect(() => {
async function fetchUser() {
try {
const userData = await api.getUser(userId);
setUser(userData);
} catch (error) {
console.error('Failed to fetch user:', error);
} finally {
setLoading(false);
}
}
fetchUser();
}, []); // Empty dependency array = run once
if (loading) return <div>Loading...</div>;
return <div>{user?.name}</div>;
}
2. Dependency-Based Effects
function SearchResults({ searchTerm }: { searchTerm: string }) {
const [results, setResults] = useState<SearchResult[]>([]);
const [loading, setLoading] = useState(false);
// Runs when searchTerm changes
useEffect(() => {
if (!searchTerm.trim()) {
setResults([]);
return;
}
setLoading(true);
async function search() {
try {
const data = await api.search(searchTerm);
setResults(data);
} catch (error) {
console.error('Search failed:', error);
} finally {
setLoading(false);
}
}
search();
}, [searchTerm]); // Runs when searchTerm changes
return (
<div>
{loading && <div>Searching...</div>}
{results.map(result => (
<div key={result.id}>{result.title}</div>
))}
</div>
);
}
3. Cleanup with Subscriptions
function LiveChat({ roomId }: { roomId: string }) {
const [messages, setMessages] = useState<Message[]>([]);
useEffect(() => {
const socket = new WebSocket(`ws://localhost:8000/chat/${roomId}`);
socket.onmessage = (event) => {
const message = JSON.parse(event.data);
setMessages(prev => [...prev, message]);
};
socket.onerror = (error) => {
console.error('WebSocket error:', error);
};
// Cleanup function - runs when component unmounts or roomId changes
return () => {
socket.close();
};
}, [roomId]);
return (
<div>
{messages.map(msg => (
<div key={msg.id}>{msg.text}</div>
))}
</div>
);
}
4. Timer and Intervals
function Timer() {
const [seconds, setSeconds] = useState(0);
const [isActive, setIsActive] = useState(false);
useEffect(() => {
let interval: NodeJS.Timeout;
if (isActive) {
interval = setInterval(() => {
setSeconds(seconds => seconds + 1);
}, 1000);
}
return () => {
if (interval) {
clearInterval(interval);
}
};
}, [isActive]); // Runs when isActive changes
return (
<div>
<div>Time: {seconds}s</div>
<button onClick={() => setIsActive(!isActive)}>
{isActive ? 'Pause' : 'Start'}
</button>
</div>
);
}
Common Mistakes and Solutions
1. Missing Dependencies
❌ Wrong - Missing dependency
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, []); // Missing userId dependency
return <div>{user?.name}</div>;
}
✅ Correct - Include all dependencies
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]); // Include userId dependency
return <div>{user?.name}</div>;
}
2. Infinite Re-renders
❌ Wrong - Object/function in dependency array
function DataFetcher({ config }: { config: RequestConfig }) {
const [data, setData] = useState(null);
useEffect(() => {
fetchData(config).then(setData);
}, [config]); // config is an object, causes infinite re-renders
return <div>{data}</div>;
}
✅ Correct - Destructure or use useMemo
function DataFetcher({ config }: { config: RequestConfig }) {
const [data, setData] = useState(null);
// Option 1: Destructure specific properties
useEffect(() => {
fetchData(config).then(setData);
}, [config.url, config.method]); // Only depend on specific values
return <div>{data}</div>;
}
// Option 2: Use useMemo for stable reference
function DataFetcher({ config }: { config: RequestConfig }) {
const [data, setData] = useState(null);
const stableConfig = useMemo(() => config, [
config.url,
config.method,
config.headers
]);
useEffect(() => {
fetchData(stableConfig).then(setData);
}, [stableConfig]);
return <div>{data}</div>;
}
3. Stale Closures
❌ Wrong - Stale closure in interval
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1); // Always uses initial count value (0)
}, 1000);
return () => clearInterval(interval);
}, []); // Empty dependencies cause stale closure
return <div>{count}</div>;
}
✅ Correct - Use functional state update
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(prevCount => prevCount + 1); // Uses current count
}, 1000);
return () => clearInterval(interval);
}, []); // Empty dependencies are fine now
return <div>{count}</div>;
}
4. Not Cleaning Up Event Listeners
❌ Wrong - Memory leak
function WindowSizeTracker() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
function handleResize() {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
}
window.addEventListener('resize', handleResize);
// Missing cleanup!
}, []);
return <div>{windowSize.width} x {windowSize.height}</div>;
}
✅ Correct - Clean up event listeners
function WindowSizeTracker() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
function handleResize() {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
}
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return <div>{windowSize.width} x {windowSize.height}</div>;
}
5. Race Conditions in Async Operations
❌ Wrong - Race condition
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
async function fetchUser() {
const userData = await api.getUser(userId);
setUser(userData); // May set wrong user if userId changed
}
fetchUser();
}, [userId]);
return <div>{user?.name}</div>;
}
✅ Correct - Handle race conditions
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
let cancelled = false;
async function fetchUser() {
try {
const userData = await api.getUser(userId);
if (!cancelled) {
setUser(userData);
}
} catch (error) {
if (!cancelled) {
console.error('Failed to fetch user:', error);
}
}
}
fetchUser();
return () => {
cancelled = true;
};
}, [userId]);
return <div>{user?.name}</div>;
}
Advanced useEffect Patterns
1. Custom Hook for Data Fetching
function useApi<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
async function fetchData() {
try {
setLoading(true);
setError(null);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (!cancelled) {
setData(result);
}
} catch (err) {
if (!cancelled) {
setError(err instanceof Error ? err.message : 'Unknown error');
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}
fetchData();
return () => {
cancelled = true;
};
}, [url]);
return { data, loading, error };
}
// Usage
function UserList() {
const { data: users, loading, error } = useApi<User[]>('/api/users');
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>
);
}
2. Debounced Effect
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(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<SearchResult[]>([]);
const debouncedSearchTerm = useDebounce(searchTerm, 300);
useEffect(() => {
if (debouncedSearchTerm) {
searchApi(debouncedSearchTerm).then(setResults);
} else {
setResults([]);
}
}, [debouncedSearchTerm]);
return (
<div>
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search..."
/>
{results.map(result => (
<div key={result.id}>{result.title}</div>
))}
</div>
);
}
3. AbortController for Cancellation
function useApiWithCancellation<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const abortController = new AbortController();
async function fetchData() {
try {
setLoading(true);
setError(null);
const response = await fetch(url, {
signal: abortController.signal
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
if (err.name === 'AbortError') {
// Request was cancelled, don't update state
return;
}
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
}
fetchData();
return () => {
abortController.abort();
};
}, [url]);
return { data, loading, error };
}
4. Multiple Effects for Separation of Concerns
function UserDashboard({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
const [posts, setPosts] = useState<Post[]>([]);
const [notifications, setNotifications] = useState<Notification[]>([]);
// Effect 1: Fetch user data
useEffect(() => {
fetchUser(userId).then(setUser);
}, [userId]);
// Effect 2: Fetch user posts
useEffect(() => {
fetchUserPosts(userId).then(setPosts);
}, [userId]);
// Effect 3: Set up notifications
useEffect(() => {
const unsubscribe = subscribeToNotifications(userId, setNotifications);
return unsubscribe;
}, [userId]);
// Effect 4: Update document title
useEffect(() => {
if (user) {
document.title = `${user.name} - Dashboard`;
}
return () => {
document.title = 'App';
};
}, [user]);
return (
<div>
{/* Component JSX */}
</div>
);
}
Performance Optimization
1. Avoid Unnecessary Re-renders
// Use useCallback for stable function references
function UserList() {
const [users, setUsers] = useState<User[]>([]);
const [filter, setFilter] = useState('');
const handleUserUpdate = useCallback((userId: string, newData: Partial<User>) => {
setUsers(prev => prev.map(user =>
user.id === userId ? { ...user, ...newData } : user
));
}, []);
useEffect(() => {
fetchUsers().then(setUsers);
}, []);
const filteredUsers = useMemo(() =>
users.filter(user => user.name.toLowerCase().includes(filter.toLowerCase()))
, [users, filter]);
return (
<div>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter users..."
/>
{filteredUsers.map(user => (
<UserItem
key={user.id}
user={user}
onUpdate={handleUserUpdate}
/>
))}
</div>
);
}
2. Conditional Effects
function ConditionalEffects({ shouldFetch, userId }: { shouldFetch: boolean; userId: string }) {
const [data, setData] = useState(null);
useEffect(() => {
if (!shouldFetch) return;
fetchUserData(userId).then(setData);
}, [shouldFetch, userId]);
return <div>{data && JSON.stringify(data)}</div>;
}
Testing useEffect
1. Testing with React Testing Library
import { render, screen, waitFor } from '@testing-library/react';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import UserProfile from './UserProfile';
const server = setupServer(
rest.get('/api/users/:userId', (req, res, ctx) => {
const { userId } = req.params;
return res(ctx.json({ id: userId, name: 'John Doe' }));
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('fetches and displays user data', async () => {
render(<UserProfile userId="123" />);
expect(screen.getByText('Loading...')).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText('John Doe')).toBeInTheDocument();
});
});
test('handles fetch error', async () => {
server.use(
rest.get('/api/users/:userId', (req, res, ctx) => {
return res(ctx.status(500), ctx.json({ error: 'Server error' }));
})
);
render(<UserProfile userId="123" />);
await waitFor(() => {
expect(screen.getByText(/error/i)).toBeInTheDocument();
});
});
2. Testing Cleanup
import { render, unmountComponentAtNode } from '@testing-library/react';
test('cleans up event listeners on unmount', () => {
const addEventListenerSpy = jest.spyOn(window, 'addEventListener');
const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener');
const { unmount } = render(<WindowSizeTracker />);
expect(addEventListenerSpy).toHaveBeenCalledWith('resize', expect.any(Function));
unmount();
expect(removeEventListenerSpy).toHaveBeenCalledWith('resize', expect.any(Function));
addEventListenerSpy.mockRestore();
removeEventListenerSpy.mockRestore();
});
Debugging useEffect
1. Add Logging
function DebugEffect({ userId }: { userId: string }) {
useEffect(() => {
console.log('Effect running for userId:', userId);
return () => {
console.log('Effect cleanup for userId:', userId);
};
}, [userId]);
return <div>User: {userId}</div>;
}
2. Use React DevTools Profiler
The React DevTools Profiler can help identify unnecessary re-renders and effect executions.
Best Practices Summary
- Always include all dependencies in the dependency array
- Use functional state updates to avoid stale closures
- Clean up side effects (timers, subscriptions, event listeners)
- Handle race conditions in async operations
- Separate concerns with multiple effects
- Use custom hooks to encapsulate reusable effect logic
- Optimize performance with useCallback and useMemo
- Test effects thoroughly including cleanup behavior