Skip to content

useEffect Hook

useEffect runs side effects after React commits DOM updates: fetching data, subscriptions, timers, and syncing with non-React systems. Cleanup functions prevent leaks when dependencies change or components unmount.

Effect Lifecycle

useEffect(callback, deps) runs the callback after paint. Return a cleanup function from the callback to run before the next effect or on unmount.

An empty dependency array [] runs once on mount (and cleanup on unmount). Omitting deps runs after every render — rarely what you want.

useEffect(() => {
  const controller = new AbortController();

  fetch(`/api/users/${userId}`, { signal: controller.signal })
    .then(r => r.json())
    .then(setUser)
    .catch(err => { if (err.name !== 'AbortError') setError(err); });

  return () => controller.abort();
}, [userId]);

AbortController cancels in-flight fetch when userId changes or component unmounts.

Dependency Array Guide

useEffect(fn)           // every render (avoid)
useEffect(fn, [])       // mount + unmount only
useEffect(fn, [a, b])   // when a or b change
  • Include every reactive value from component scope used inside the effect.
  • Exhaustive deps rule from ESLint prevents stale effect bugs.
  • Cleanup runs before re-running effect and on unmount.
  • Prefer event handlers over effects for user-triggered logic.

useEffect Use Cases

When useEffect is — and is not — appropriate.

Use Case Example
Fetch on mount/dep change API call with abort cleanup
Subscriptions WebSocket, event listeners
Timers setInterval with clearInterval cleanup
DOM sync Document title, third-party widgets
NOT for derived state Compute during render instead
NOT for user clicks Use event handlers

Effects vs Event Handlers

If something should happen because the user clicked a button, use onClick. If something should happen because userId prop changed, use useEffect. Misusing effects for click logic creates unnecessary runs and harder-to-follow code.

Replacing Fetch Effects with TanStack Query

Manual fetch + effect + loading state is repetitive. TanStack Query (React Query) handles caching, refetching, and deduplication — see the React Query lesson.

const { data, isLoading, error } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId),
});

Common Mistakes

  • Missing dependencies causing stale data bugs.
  • Infinite loops from setting state in effects without proper deps.
  • Not cleaning up subscriptions and timers.
  • Using effects for transformations that belong in render.

Key Takeaways

  • useEffect handles side effects after render.
  • Always consider cleanup for subscriptions and async work.
  • Dependency arrays control when effects re-run.
  • Prefer event handlers or TanStack Query when appropriate.

Pro Tip

If your effect only sets state derived from props, delete the effect and compute during render instead.