This article is published in English.
Stop defaulting to useEffect for every React problem
Effects sync with external systems; derive in render, handle clicks in events, fetch with real data tools.
The useEffect trap
Hooks learners often reach for useEffect for fetching, derived state, and button clicks. Effects synchronize with external systems—they are not the default hammer for ordinary render logic.
What useEffect actually does
After paint, React runs effects to attach listeners, open connections, or imperatively talk to something outside React. Misuse creates flicker, double-fetch, and race bugs.
// ❌ BAD: Two renders, unnecessary state
function UserProfile({ firstName, lastName }) {
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
return <h1>{fullName}</h1>;
}
// ✅ GOOD: Calculate during render
function UserProfile({ firstName, lastName }) {
const fullName = `${firstName} ${lastName}`;
return <h1>{fullName}</h1>;
}
// ❌ BAD: Unnecessary effect + state
function ProductList({ products }) {
const [sortedProducts, setSortedProducts] = useState([]);
useEffect(() => {
const sorted = [...products].sort((a, b) => a.price - b.price);
setSortedProducts(sorted);
}, [products]);
return (
<ul>
{sortedProducts.map(product => (
<li key={product.id}>{product.name} - ${product.price}</li>
))}
</ul>
);
}
// ✅ GOOD: Calculate during render (useMemo for expensive ops)
function ProductList({ products }) {
const sortedProducts = useMemo(() => {
return [...products].sort((a, b) => a.price - b.price);
}, [products]);
return (
<ul>
{sortedProducts.map(product => (
<li key={product.id}>{product.name} - ${product.price}</li>
))}
</ul>
);
}
// ❌ BAD: Manual fetching with useEffect
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
async function fetchUser() {
try {
setLoading(true);
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('Failed to fetch');
const data = await response.json();
if (!cancelled) setUser(data);
} catch (err) {
if (!cancelled) setError(err.message);
} finally {
if (!cancelled) setLoading(false);
}
}
fetchUser();
return () => { cancelled = true; };
}, [userId]);
if (loading) return <Spinner />;
if (error) return <p>Error: {error}</p>;
return <div>{user.name}</div>;
}
// ✅ GOOD: Use TanStack Query (React Query)
import { useQuery } from '@tanstack/react-query';
function UserProfile({ userId }) {
const { data: user, isPending, error } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
});
if (isPending) return <Spinner />;
if (error) return <p>Error loading user</p>;
return <div>{user.name}</div>;
}
// ❌ BAD: Overcomplicating with useEffect
function LikeButton({ postId }) {
const [liked, setLiked] = useState(false);
useEffect(() => {
if (liked) {
fetch(`/api/posts/${postId}/like`, { method: 'POST' });
} else {
fetch(`/api/posts/${postId}/unlike`, { method: 'POST' });
}
}, [liked, postId]);
return (
<button onClick={() => setLiked(!liked)}>
{liked ? '❤️' : '🤍'}
</button>
);
}
// ✅ GOOD: Handle in the event handler
function LikeButton({ postId }) {
const [liked, setLiked] = useState(false);
const handleLike = async () => {
const newLiked = !liked;
setLiked(newLiked);
await fetch(`/api/posts/${postId}/${newLiked ? 'like' : 'unlike'}`, {
method: 'POST',
});
};
return (
<button onClick={handleLike}>
{liked ? '❤️' : '🤍'}
</button>
);
}
function WindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
// Cleanup is crucial!
return () => window.removeEventListener('resize', handleResize);
}, []);
return <p>Window width: {width}px</p>;
}
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createWebSocketConnection(roomId);
connection.onMessage = (msg) => {
setMessages(prev => [...prev, msg]);
};
connection.connect();
return () => {
connection.disconnect(); // Cleanup!
};
}, [roomId]);
return <MessageList messages={messages} />;
}
function ChartComponent({ data }) {
const chartRef = useRef(null);
const chartInstance = useRef(null);
useEffect(() => {
if (chartRef.current && !chartInstance.current) {
chartInstance.current = new ChartLibrary(chartRef.current, {
type: 'line',
data: data,
});
}
return () => {
chartInstance.current?.destroy();
chartInstance.current = null;
};
}, []);
return <canvas ref={chartRef} />;
}
function PageTitle({ title }) {
useEffect(() => {
document.title = `${title} | My App`;
return () => {
document.title = 'My App'; // Reset on unmount
};
}, [title]);
return null;
}
Four common mistakes
1. Calculating values in effects
Derive during render (or useMemo when expensive). Effects that setState from props/state often encode forbidden feedback loops.
2. Expensive calculations in effects
Keep pure computation in render/memo, not after paint.
3. Data fetching as an ungoverned effect
Fetching needs cancellation, caching, and error UX. Prefer dedicated libraries or frameworks with loading boundaries; if using effects, handle abort and races.
4. User actions in effects
Click handlers belong on events. Effects that fire because a flag flipped from a click add indirection and bugs.
Decision tree
If it calculates from existing state/props → render. If it responds to a click/submit → event handler. If it talks to a DOM API, network feed, or third-party widget lifecycle → effect.
When effects are right
External event listeners
WebSockets
Third-party library initialization
Imperative non-React APIs that must dispose cleanly
Closing
Fewer effects, clearer data flow. Reserve useEffect for synchronization with systems outside React—not as a default hammer for every post-render thought.
Strict Mode double-invokes effects in development to surface missing cleanups—treat that as a feature when wiring sockets. Prefer declarative data libraries for server state so components describe dependencies instead of hand-rolling fetch effects. Measure before adding effects that only copy props into state; that pattern is usually a misunderstanding of render.
Cleanup functions must tear down listeners and timers; lingering handlers are a classic source of setState-on-unmounted warnings.
Dependency arrays are part of the contract—omit values and stale closures appear; over-list objects and effects thrash every render.
When synchronizing with URL search params or external stores, prefer purpose-built hooks that encapsulate the effect boundary.
Server-rendered apps should not start browser-only effects without guards; gate on environment or lifecycle.
Cleanup functions must tear down listeners and timers; lingering handlers are a classic source of setState-on-unmounted warnings.
Dependency arrays are part of the contract—omit values and stale closures appear; over-list objects and effects thrash every render.
When synchronizing with URL search params or external stores, prefer purpose-built hooks that encapsulate the effect boundary.
Server-rendered apps should not start browser-only effects without guards; gate on environment or lifecycle.
Cleanup functions must tear down listeners and timers; lingering handlers are a classic source of setState-on-unmounted warnings.
Dependency arrays are part of the contract—omit values and stale closures appear; over-list objects and effects thrash every render.
When synchronizing with URL search params or external stores, prefer purpose-built hooks that encapsulate the effect boundary.
Server-rendered apps should not start browser-only effects without guards; gate on environment or lifecycle.
Cleanup functions must tear down listeners and timers; lingering handlers are a classic source of setState-on-unmounted warnings.
Dependency arrays are part of the contract—omit values and stale closures appear; over-list objects and effects thrash every render.
When synchronizing with URL search params or external stores, prefer purpose-built hooks that encapsulate the effect boundary.
Server-rendered apps should not start browser-only effects without guards; gate on environment or lifecycle.
Cleanup functions must tear down listeners and timers; lingering handlers are a classic source of setState-on-unmounted warnings.
Dependency arrays are part of the contract—omit values and stale closures appear; over-list objects and effects thrash every render.
When synchronizing with URL search params or external stores, prefer purpose-built hooks that encapsulate the effect boundary.
Server-rendered apps should not start browser-only effects without guards; gate on environment or lifecycle.
Cleanup functions must tear down listeners and timers; lingering handlers are a classic source of setState-on-unmounted warnings.
Dependency arrays are part of the contract—omit values and stale closures appear; over-list objects and effects thrash every render.
When synchronizing with URL search params or external stores, prefer purpose-built hooks that encapsulate the effect boundary.
Server-rendered apps should not start browser-only effects without guards; gate on environment or lifecycle.
Cleanup functions must tear down listeners and timers; lingering handlers are a classic source of setState-on-unmounted warnings.
Dependency arrays are part of the contract—omit values and stale closures appear; over-list objects and effects thrash every render.
When synchronizing with URL search params or external stores, prefer purpose-built hooks that encapsulate the effect boundary.
Server-rendered apps should not start browser-only effects without guards; gate on environment or lifecycle.
Cleanup functions must tear down listeners and timers; lingering handlers are a classic source of setState-on-unmounted warnings.