useEffect & Data Fetching
useEffect lets a component synchronize with systems outside React—the network, timers, browser APIs, subscriptions. It’s also the most misunderstood Hook. This page covers how it actually works and how to fetch data with it correctly.
Mechanics: effects run after render
React renders your component (computing JSX), commits to the DOM, paints the screen, and then runs your effects. Effects never block the browser from painting.
useEffect(() => {
document.title = `Inbox (${count})`;
});
With no dependency array, this runs after every render. That’s rarely what you want—use the dependency array to scope it.
The dependency array
The second argument tells React when to re-run the effect by comparing each dependency to its previous value:
useEffect(() => { /* ... */ }, [userId]); // runs when userId changes
useEffect(() => { /* ... */ }, []); // runs once, on mount
useEffect(() => { /* ... */ }); // runs after every render
List every value the effect reads from component scope—props, state, functions. Lying to the dependency array causes stale closures, where the effect captures an old value and silently misbehaves. The lint plugin flags omissions.
Cleanup
If an effect creates something that must be torn down—a subscription, timer, or listener—return a cleanup function. React runs it before the next effect run and on unmount.
useEffect(() => {
function onResize() { setWidth(window.innerWidth); }
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, []);
Forgetting cleanup is the classic source of memory leaks and duplicate subscriptions.
Why effects run twice in StrictMode
In development, React’s <StrictMode> deliberately mounts, unmounts, and remounts each component once, so every effect runs setup → cleanup → setup. This isn’t a bug—it’s a stress test that surfaces effects missing proper cleanup. If your effect is correctly symmetric (every setup has a matching teardown), the double-invoke is harmless. It does not happen in production builds.
Data fetching with loading and error state
A robust fetch effect tracks three things: the data, a loading flag, and an error. Here it is in TypeScript:
interface User { id: string; name: string; }
function UserCard({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const controller = new AbortController();
setLoading(true);
setError(null);
fetch(`/api/users/${userId}`, { signal: controller.signal })
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then((data: User) => setUser(data))
.catch((err) => {
if (err.name !== "AbortError") setError(err.message);
})
.finally(() => setLoading(false));
return () => controller.abort(); // cancel on unmount or userId change
}, [userId]);
if (loading) return <Spinner />;
if (error) return <p role="alert">Failed: {error}</p>;
return <h2>{user?.name}</h2>;
}
AbortController and race conditions
When userId changes rapidly, multiple requests are in flight at once. Without cancellation, a slow earlier response can resolve after a faster later one and overwrite it—a race condition. The cleanup function calls controller.abort(), canceling the stale request; we ignore the resulting AbortError. This is essential for correctness, not just tidiness.
When not to use an effect
Effects are for synchronizing with external systems—not for transforming data for rendering. Common anti-patterns:
| Instead of an effect that… | Do this |
|---|---|
| Computes a value from props/state | Calculate it during render |
| Resets state when a prop changes | Use a key to remount the component |
| Handles a user action (e.g. POST on click) | Put the logic in the event handler |
For real applications, prefer a dedicated data library—TanStack Query or SWR—which handle caching, deduplication, retries, and race conditions for you. The manual pattern above is what they’re built on.
Best Practices
- Include every dependency the effect reads; trust the lint rule.
- Always return cleanup for subscriptions, timers, and listeners.
- Use
AbortControllerto cancel in-flight requests and prevent races. - Keep
<StrictMode>on in development—it exposes missing cleanup early. - Don’t use effects for data you can derive during render or logic that belongs in event handlers.
- For production data fetching, reach for TanStack Query or SWR instead of hand-rolling.