useEffect

Master useEffect — the hook for synchronising React components with the outside world. Learn the dependency array, cleanup functions, and how to avoid the most common production bugs.

Last reviewed: January 2025

·React 16.8+React 18React 19
✅
Quick Understanding: useEffect lets you run code after a component renders — for things that cannot happen during rendering: API calls, subscriptions, timers, DOM manipulation. The dependency array controls when it runs. The cleanup function prevents memory leaks when the component unmounts.

🔍 What Is It?

useEffect(fn, deps) runs fn after the component renders. It is React's escape hatch for synchronising with external systems.

Three forms:

  • →useEffect(fn) — runs after every render
  • →useEffect(fn, []) — runs once after the first render (mount)
  • →useEffect(fn, [dep1, dep2]) — runs after renders where dep1 or dep2 changed

Cleanup function:

Return a function from the effect to clean up before the next run or on unmount:

useEffect(() => {
  const sub = subscribe(channel);
  return () => sub.unsubscribe(); // cleanup
}, [channel]);

🎯 Why Do We Need It?

React rendering must be a pure function — no side effects during render. useEffect is the designated place for side effects:

  • →Data fetching — calling an API when a component mounts or a filter changes
  • →Subscriptions — WebSocket connections, event listeners, Observables
  • →Timers — setInterval, setTimeout
  • →DOM manipulation — measuring elements, integrating third-party libraries
  • →Synchronising external state — syncing React state to localStorage

Without useEffect, you would have no way to trigger these after rendering.

🏗️ What Happens in Real Projects?

In a real SaaS dashboard:

  • →Fetch dashboard metrics when the selected date range changes
  • →Subscribe to a WebSocket for real-time notifications
  • →Sync filter state to URL query params
  • →Set document.title to the current page name
  • →Initialise a charting library (Chart.js, D3) on a canvas element
  • →Clean up event listeners when a modal closes

⚙️ How It Works

Execution order:

  1. Component renders (JSX evaluated, DOM updated)
  2. Browser paints the screen
  3. useEffect runs
  4. On next render: cleanup of previous effect runs, then new effect runs
  5. On unmount: cleanup of last effect runs

Dependency array rules:

  • →React uses Object.is() to compare dependencies
  • →Objects and arrays are compared by reference, not value
  • →The ESLint rule react-hooks/exhaustive-deps catches missing dependencies — always follow it
  • →If a dependency changes every render (e.g., an inline object), the effect runs every render too

💻 Example

useEffect Patterns in Production — Data fetching, subscriptions, and cleanup patterns from real projects.

tsx
function useUserProfile(userId: string) {
  const [profile, setProfile] = useState<UserProfile | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    // Cancellation flag prevents setState on unmounted component
    let cancelled = false;

    setLoading(true);
    setError(null);

    fetchUserProfile(userId)
      .then((data) => {
        if (!cancelled) {
          setProfile(data);
          setLoading(false);
        }
      })
      .catch((err) => {
        if (!cancelled) {
          setError(err.message);
          setLoading(false);
        }
      });

    return () => { cancelled = true; };
  }, [userId]); // re-fetches when userId changes

  return { profile, loading, error };
}

🤔 Common Confusions

❓ Why does my useEffect run twice in development?

React 18 Strict Mode intentionally mounts, unmounts, and remounts every component in development to help you find missing cleanup functions. This only happens in development — never in production. If your effect breaks on double-mount, your cleanup function is missing or incorrect.

❓ What happens if I omit a dependency from the array?

The effect captures a stale closure — it sees the value of the dependency from the render it was created in, not the current value. This causes subtle bugs where the effect uses outdated data. Always follow the exhaustive-deps ESLint rule. If adding a dependency causes an infinite loop, the real fix is restructuring the effect, not removing the dependency.

❓ Should I use useEffect for data fetching?

For simple cases, yes. For production apps, use React Query (TanStack Query) instead. It handles caching, background refetching, deduplication, loading/error states, and stale-while-revalidate automatically. Manual useEffect data fetching is error-prone and lacks these features.

🚨 Common Production Issues

**Memory leak: setState on unmounted component** — An async operation completes after the component unmounts and calls setState. Fix: use a cancellation flag in the cleanup function.

**Infinite loop: Object in dependency array** — useEffect(fn, [{ id: 1 }]) creates a new object every render, causing the effect to run every render. Fix: depend on primitives (id) not objects.

**Bug: Missing cleanup for subscriptions** — Event listeners, WebSockets, and intervals that are not cleaned up on unmount cause memory leaks and ghost callbacks. Always return a cleanup function.

**Bug: Race condition** — User changes a filter rapidly; multiple API calls are in flight; the last one to resolve wins, which may not be the most recent request. Fix: cancellation flag or AbortController.

✅ Best Practices

Always return a cleanup function for subscriptions, timers, and event listeners

Use a cancellation flag or AbortController for async operations

Follow the exhaustive-deps ESLint rule — never suppress it without understanding why

Depend on primitive values (id, string) not objects or arrays

Extract complex effects into custom hooks for reusability and testability

For data fetching in production, prefer React Query over manual useEffect

Never fetch data in useEffect with an empty dependency array in components that receive dynamic IDs via props

🏛️ Architect's Note

Senior Architect's Perspective

useEffect is the most misused hook in React. The most common mistake is treating it like componentDidMount — a place to run code once. React 18 and Strict Mode make it clear: every effect must be resilient to running twice and must clean up after itself. If you find yourself fighting useEffect, the answer is usually a custom hook or React Query, not a more clever effect. In large codebases, raw useEffect for data fetching is a code smell — it should be encapsulated in a custom hook or replaced with a data fetching library.

💬 Frequently Asked Questions

What is the difference between useEffect and useLayoutEffect?

useEffect runs after the browser paints (non-blocking). useLayoutEffect runs synchronously after DOM updates but before the browser paints (blocking). Use useLayoutEffect only when you need to measure DOM elements or prevent visual flicker from a DOM mutation. For everything else, useEffect is correct and has better performance.

Can I use async/await directly in useEffect?

No. useEffect's callback cannot be async because async functions return a Promise, but useEffect expects either undefined or a cleanup function. Fix: define an async function inside the effect and call it immediately. Or use .then() chains.

How do I run an effect only once?

Pass an empty dependency array: useEffect(fn, []). This runs after the first render only. In React 18 Strict Mode (development only), it will run twice to verify cleanup. If your once-only effect breaks on double-run, add a cleanup function.

🎯 Key Takeaways

1useEffect runs after render — never during render
2The dependency array controls when the effect runs
3Always return a cleanup function for subscriptions, timers, and listeners
4React 18 Strict Mode runs effects twice in development — this is intentional
5Missing dependencies cause stale closure bugs — always follow exhaustive-deps
6Object/array dependencies cause infinite loops — depend on primitives
7For production data fetching, use React Query instead of raw useEffect

🎯 Practice this topic in interviews

↗ Where this topic fits

Sources

Sources / References

Use these React references to verify the concept in official docs and continue into the most relevant SplashRide pages.

Related SplashRide pages

SplashRide React learning path

Browse the full React topic collection and learning order on SplashRide.

React Interview Prep

Practice React interview questions and scenario-based answers.

Frontend Engineer Career Path

A beginner-friendly journey through web foundations, React, production frontend engineering, and interview preparation.

Python Developer Career Path

A practical route through Python fundamentals, automation, backend basics, testing, projects, interview preparation, and career growth.

useState

Master useState — the most fundamental React hook. Learn how it works under the hood, the patterns senior engineers use, and the bugs that trip up even experienced developers.

useCallback

Master useCallback — the hook for stabilising function references between renders. Learn when it genuinely helps performance and when it is unnecessary overhead.