What Are the Legitimate Use Cases for useEffect in React?
Problem: My useEffect Hook Is Triggering Unexpected Re-renders
I was debugging a React component last week when I realized I had fallen into a common trap. My code looked something like this:
function UserProfile({ userId }) { const [user, setUser] = useState(null); const [posts, setPosts] = useState([]);
useEffect(() => { // Effect 1: Fetch user when userId changes fetchUser(userId).then(setUser); }, [userId]);
useEffect(() => { // Effect 2: Fetch posts when user changes if (user) { fetchPosts(user.id).then(setPosts); } }, [user]);
// ... render logic}The cascade of effects was causing unnecessary re-renders and network requests. A fellow developer on Reddit confessed to the same pattern: “If prop A changed, I had an effect to update state B, which naturally triggered another effect to fetch some data.”
This made me ask: What are the legitimate use cases for useEffect, and when am I just overcomplicating my code?
Purpose: Understanding When useEffect Is Truly Necessary
After diving into the React documentation and community discussions, I discovered that useEffect has a very specific purpose: synchronizing with external systems. Most of my useEffect hooks were solving problems that didn’t require effects at all.
The Golden Rule: External System Synchronization
The React team defines legitimate useEffect use cases as operations that need to “step outside” React’s rendering cycle to interact with the external world:
- Browser APIs (DOM measurements, event listeners)
- Third-party libraries (charting, animation libraries)
- Network requests (fetching data from servers)
- Subscriptions (WebSocket connections, observables)
- Timers (setTimeout, setInterval)
- External stores (localStorage, IndexedDB)
If your code only transforms or computes values from React state/props, you don’t need useEffect.
Legitimate Use Case 1: Browser API Integration
Browser APIs like window.addEventListener or document manipulations require cleanup and are external to React’s lifecycle.
import { useState, useEffect } from 'react';
function useWindowWidth() { const [width, setWidth] = useState( typeof window !== 'undefined' ? window.innerWidth : 0 );
useEffect(() => { // This is a legitimate effect: subscribing to browser API const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
// Cleanup is essential for browser event listeners return () => { window.removeEventListener('resize', handleResize); }; }, []); // Empty deps: run once on mount
return width;}This is legitimate because:
window.addEventListeneris external to React- We need to clean up the subscription on unmount
- The effect synchronizes React state with browser state
Legitimate Use Case 2: Third-Party Library Integration
When integrating non-React libraries, useEffect bridges the gap between React’s declarative model and imperative library APIs.
import { useEffect, useRef } from 'react';import Chart from 'chart.js';
function ChartComponent({ data, options }) { const canvasRef = useRef(null); const chartRef = useRef(null);
useEffect(() => { // Create Chart.js instance const ctx = canvasRef.current.getContext('2d'); chartRef.current = new Chart(ctx, { type: 'line', data: data, options: options });
// Cleanup Chart.js instance return () => { if (chartRef.current) { chartRef.current.destroy(); } }; }, [data, options]);
return <canvas ref={canvasRef} />;}Chart.js doesn’t know about React’s rendering cycle. We use useEffect to synchronize the Chart.js instance with React’s component lifecycle.
Legitimate Use Case 3: WebSocket Subscriptions
Real-time data subscriptions require connecting to external systems and cleaning up when the component unmounts.
import { useEffect, useState } from 'react';
function useWebSocket(url) { const [messages, setMessages] = useState([]); const [connectionStatus, setConnectionStatus] = useState('connecting');
useEffect(() => { const socket = new WebSocket(url);
socket.onopen = () => setConnectionStatus('connected'); socket.onmessage = (event) => { setMessages(prev => [...prev, JSON.parse(event.data)]); }; socket.onerror = () => setConnectionStatus('error'); socket.onclose = () => setConnectionStatus('disconnected');
// Cleanup: close the WebSocket connection return () => { socket.close(); }; }, [url]);
return { messages, connectionStatus };}This effect is essential because:
- WebSocket connections are external resources
- They need explicit cleanup to prevent memory leaks
- The connection lifecycle doesn’t align with React’s render cycle
Legitimate Use Case 4: External Store Synchronization
When reading from or writing to external stores like localStorage, useEffect ensures React state stays synchronized.
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) { // Initialize state const [storedValue, setStoredValue] = useState(() => { try { const item = window.localStorage.getItem(key); return item ? JSON.parse(item) : initialValue; } catch (error) { console.error('Error reading localStorage:', error); return initialValue; } });
// Synchronize to localStorage when value changes useEffect(() => { try { window.localStorage.setItem(key, JSON.stringify(storedValue)); } catch (error) { console.error('Error writing to localStorage:', error); } }, [key, storedValue]);
return [storedValue, setStoredValue];}Note: For more complex external store synchronization, consider using useSyncExternalStore introduced in React 18.
Common Anti-Patterns: When NOT to Use useEffect
After reviewing my code, I found several patterns where I was using useEffect unnecessarily.
Anti-Pattern 1: Derived State
// ❌ WRONG: Using effect for derived statefunction UserList({ users }) { const [sortedUsers, setSortedUsers] = useState([]);
useEffect(() => { setSortedUsers([...users].sort((a, b) => a.name.localeCompare(b.name))); }, [users]);
return <ul>{sortedUsers.map(u => <li key={u.id}>{u.name}</li>)}</ul>;}
// ✅ CORRECT: Derive state during renderfunction UserList({ users }) { const sortedUsers = [...users].sort((a, b) => a.name.localeCompare(b.name) );
return <ul>{sortedUsers.map(u => <li key={u.id}>{u.name}</li>)}</ul>;}Anti-Pattern 2: Handling Events
// ❌ WRONG: Using effect to respond to eventsfunction SubmitButton({ onSubmit }) { const [submitted, setSubmitted] = useState(false);
useEffect(() => { if (submitted) { onSubmit(); setSubmitted(false); } }, [submitted, onSubmit]);
return ( <button onClick={() => setSubmitted(true)}> Submit </button> );}
// ✅ CORRECT: Handle events directly in event handlerfunction SubmitButton({ onSubmit }) { const handleSubmit = () => { onSubmit(); };
return ( <button onClick={handleSubmit}> Submit </button> );}Anti-Pattern 3: Data Fetching Without Caching
While data fetching is a legitimate use case, raw useEffect can lead to race conditions and lacks caching.
// ⚠️ RISKY: Manual data fetching can cause issuesfunction UserProfile({ userId }) { const [user, setUser] = useState(null);
useEffect(() => { fetch(`/api/users/${userId}`) .then(res => res.json()) .then(setUser); }, [userId]);
return user ? <div>{user.name}</div> : <div>Loading...</div>;}A community comment from Reddit highlights this: “It depends, some still need useEffect. You can extract to use some lib like tanstack Query or else or make your own hook.”
The better approach:
// ✅ BETTER: Use a data fetching libraryimport { useQuery } from '@tanstack/react-query';
function UserProfile({ userId }) { const { data: user, isLoading } = useQuery({ queryKey: ['user', userId], queryFn: () => fetch(`/api/users/${userId}`).then(res => res.json()) });
return isLoading ? <div>Loading...</div> : <div>{user.name}</div>;}Key Insights from the Community
The Reddit discussion revealed several important perspectives:
On Architecture:
“A lot of these issues arise from having stuff that doesn’t belong in the UI layer. Shared data, business logic, network code etc. all belong outside React runtime.”
This suggests that the problem isn’t useEffect itself, but where we place our business logic.
On Data Fetching:
“This is why pure components are so important. Don’t fetch data at all, get it from the props.”
Moving data fetching to a parent component or a state management layer can eliminate many useEffect hooks.
On When Effects Are Needed:
“Sometimes they’re legit, let’s say setting initial state on page load based on some external variable (e.g. URL params) or syncing with an external store.”
External synchronization remains the core legitimate use case.
Summary: The useEffect Decision Tree
Before adding a useEffect hook, ask yourself:
┌─────────────────────────────────────────┐│ Do I need to interact with an external ││ system (browser API, network, etc.)? │└─────────────┬───────────────────────────┘ │ ┌────────┴────────┐ │ │ YES NO │ │ ▼ ▼┌──────────┐ ┌──────────────────┐│ useEffect │ │ Can I compute ││ may be │ │ during render? ││ needed │ └────────┬─────────┘└──────────┘ │ ┌──────┴──────┐ │ │ YES NO │ │ ▼ ▼ ┌──────────┐ ┌──────────────┐ │ Derive │ │ Is this in │ │ directly │ │ event handler?│ │ in render│ └──────┬───────┘ └──────────┘ │ ┌──────┴──────┐ │ │ YES NO │ │ ▼ ▼ ┌──────────┐ ┌──────────────┐ │ Put in │ │ Reconsider │ │ onClick/ │ │ architecture │ │ onChange │ │ or use lib │ └──────────┘ └──────────────┘Final Recommendations
- Use useEffect for: Browser APIs, third-party libraries, subscriptions, WebSocket connections, external store synchronization
- Avoid useEffect for: Derived state, event handling, simple state transformations
- Consider libraries for: Data fetching (TanStack Query), form state (React Hook Form), router state (React Router)
- Remember cleanup: Always return a cleanup function for subscriptions and event listeners
The key principle remains: useEffect synchronizes React with the external world. If you’re not leaving React’s ecosystem, you probably don’t need it.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments