How to Run Code When a Component Mounts in React

Beginner8 min interview
Skills tested:
Using useEffect with an empty dependency array to run logic once on mountUnderstanding why the empty array is required and what happens without itWriting a cleanup function that runs on unmountKnowing the equivalence between useEffect([]) and componentDidMountHandling the React Strict Mode double-invocation behavior in development

Advertisement

🧩 Scenario

Running code on mount is one of the most common React patterns: fetching initial data, subscribing to a WebSocket, registering keyboard shortcuts, initializing a third-party library (charts, maps, editors). All of these need to run once when the component appears and clean up when it disappears.

Architecture Walkthrough

The Empty Dependency Array

useEffect accepts two arguments: a callback and a dependency array. The dependency array controls when the effect re-runs. When the array is empty ([]), React compares the deps on each render and finds nothing has changed, so the effect runs only after the first render and never again. This is the functional component equivalent of componentDidMount.

Without the dependency array, the effect runs after every render. With a non-empty dependency array, it runs after the first render and after any render where one of the listed values changed. The empty array is the intentional way to say "run once and only once."

Cleanup on Unmount

The callback passed to useEffect can return a cleanup function. When the dependency array is empty, the cleanup function runs once: when the component unmounts. This is the equivalent of componentWillUnmount. Use the cleanup function to cancel timers, remove event listeners, unsubscribe from observables, and abort in-flight requests. Failing to clean up creates memory leaks and stale callbacks that fire after the component is gone.

Async in useEffect

useEffect callbacks must either return a cleanup function or return nothing (undefined). An async function implicitly returns a Promise, which is neither. Passing an async function directly to useEffect causes React to ignore the returned Promise (including any cleanup it contains) and produces a warning. The correct pattern is to define an async function inside the callback and call it immediately.


Key Code Explained

function Dashboard({ userId }: { userId: string }) {
  const [user, setUser] = useState<User | null>(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    // Define async logic inside — cannot make the callback itself async
    let cancelled = false;

    async function loadUser() {
      try {
        setIsLoading(true);
        const data = await fetchUser(userId);
        if (!cancelled) {
          setUser(data);
        }
      } finally {
        if (!cancelled) {
          setIsLoading(false);
        }
      }
    }

    loadUser();

    // Cleanup: runs when component unmounts
    // Prevents setState call on an unmounted component
    return () => {
      cancelled = true;
    };
  }, []); // [] = run only on mount

  if (isLoading) return <Skeleton />;
  return <UserProfile user={user} />;
}


// Event listener example: add on mount, remove on unmount
function KeyboardShortcuts({ onSave, onClose }: { onSave: () => void; onClose: () => void }) {
  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      if (e.key === 's' && (e.metaKey || e.ctrlKey)) {
        e.preventDefault();
        onSave();
      }
      if (e.key === 'Escape') {
        onClose();
      }
    };

    document.addEventListener('keydown', handleKeyDown);

    // Cleanup: remove the listener on unmount
    return () => {
      document.removeEventListener('keydown', handleKeyDown);
    };
  }, []); // onSave and onClose are intentionally omitted here for a mount-only setup
         // In production, you'd add them as deps or use useEffectEvent

  return null;
}


// Third-party library initialization
function ChartContainer({ data }: { data: number[] }) {
  const canvasRef = useRef<HTMLCanvasElement>(null);

  useEffect(() => {
    if (!canvasRef.current) return;

    // Initialize chart library once on mount
    const chart = new Chart(canvasRef.current, {
      type: 'line',
      data: { datasets: [{ data }] },
    });

    // Destroy chart on unmount to free memory
    return () => {
      chart.destroy();
    };
  }, []); // Mount only — data updates handled separately

  return <canvas ref={canvasRef} />;
}

The cancelled flag in the data fetching example is a lightweight alternative to AbortController. When the component unmounts before the fetch resolves, cancelled is set to true by the cleanup function. The if (!cancelled) guards prevent setState from being called on the unmounted component. A more robust approach is to use AbortController: pass signal to fetch, and call controller.abort() in the cleanup function to actually cancel the in-flight HTTP request.


Tradeoffs

useEffect depsWhen it runsCleanup fires when
No arrayAfter every renderBefore next effect run
[] (empty)After first render only (mount)On unmount
[a, b] (non-empty)After mount + when a or b changesBefore next run + unmount

What Interviewers Actually Check

  • Whether you know the empty dependency array is what makes useEffect equivalent to componentDidMount
  • Whether you write a cleanup function for anything added on mount
  • Whether you know why useEffect cannot be async directly and how to work around it
  • Whether you handle stale fetch results when the component unmounts before the request completes
  • Whether you know about the React Strict Mode double-invocation in development

Follow-Up Questions

  1. If useEffect with [] only runs once, how do you update the chart from the library initialization example when the data prop changes?
  2. How does AbortController improve on the cancelled flag for in-flight fetch cleanup?
  3. In React 19, use(promise) suspends a component during data loading. How does this change the mount-fetch pattern?
  4. If the cleanup function from a mount effect needs to close over data that was set during the effect, how do you structure that with a useRef?
  5. What does the React Strict Mode double-mount tell you about your cleanup function if state is different after the second mount than after the first?

Common Candidate Mistakes

  • Writing useEffect(() => { fetchData(); }) without the empty array and running the fetch on every render
  • Subscribing to a WebSocket or adding an event listener on mount without a cleanup function that removes it on unmount
  • Marking the useEffect callback itself as async instead of defining an async function inside it
  • Not handling the case where the component unmounts before an async operation completes, leading to state updates on unmounted components
  • Removing React Strict Mode to avoid the double-mount in development instead of writing a correct cleanup function

Interview Readiness Checklist

Before you leave this question, make sure you can answer:

  • Can you write a useEffect with an empty dependency array that runs only on mount?
  • Can you write a cleanup function that correctly tears down resources added on mount?
  • Can you explain why the empty dependency array controls when the effect runs?
  • Can you handle stale async results using a cancelled flag or AbortController?
  • Can you explain what React Strict Mode's double-invocation is testing for?

Summary

useEffect with an empty dependency array runs once after the component's first render and never again. This is the functional component equivalent of componentDidMount. The empty array tells React there are no values to watch for changes, so the effect never needs to re-run.

The useEffect callback can return a cleanup function. When the dependency array is empty, this cleanup function runs once: when the component unmounts. This is equivalent to componentWillUnmount. Failing to provide a cleanup for subscriptions, event listeners, timers, or third-party library instances creates memory leaks and callbacks that fire on unmounted components.

useEffect callbacks cannot be async functions because async functions return Promises, not cleanup functions. Define an async function inside the callback and call it immediately. For data fetching, guard the setState calls with an AbortController signal or a cancelled flag that the cleanup function sets to true, preventing state updates after the component unmounts.

Related Videos
Watch these videos to learn more about this topic
Frequently Asked Questions

Why does React Strict Mode run mount effects twice in development?

React Strict Mode intentionally mounts, unmounts, and remounts components to help you find effects that do not clean up correctly. The double invocation only happens in development, not in production.

Advertisement


Stay Updated

Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.

Advertisement