Back to Blog
Lesson 28 of the React Fundamentals: Build Modern UIs from Scratch course
ReactJune 25, 20263 min read

Cleanup Functions in useEffect: Preventing Memory Leaks

Master cleanup functions in useEffect to handle component unmounting and cancel pending API requests, ensuring your React app stays stable and bug-free.

ReactuseEffectcleanupmemory leakshooksJavaScriptfrontend

Previously in this course, we covered fetching data from an API and learned how to control effect execution using the dependency array. Today, we’re leveling up: we’ll handle the "aftermath" of those effects by mastering cleanup functions.

When you trigger a side effect—like setting up a timer or starting a network request—that effect doesn't automatically vanish when your component disappears. If you don't clean it up, you risk memory leaks or trying to update the state of a component that no longer exists.

The Mental Model: Why We Need Cleanup

In React, the lifecycle of a component is dynamic. A user might navigate away from your movie browser before an API request finishes, or they might toggle a filter that triggers a re-render. If your useEffect is still "listening" or "waiting" for data, you end up with stale logic running in the background.

A cleanup function is a mechanism provided by useEffect to "undo" the work done by the effect. React runs this function in two scenarios:

  1. Before the component unmounts: When the user leaves the page or the component is removed from the DOM.
  2. Before the effect re-runs: If your dependencies change, React cleans up the previous effect before executing the new one.

How to Implement a Cleanup Function

To define a cleanup function, you simply return a function from within your useEffect callback.

JAVASCRIPT
useEffect(() => {
  // 1. Setup the effect
  const timer = setInterval(() => {
    console.log("Tick");
  }, 1000);

  // 2. Return the cleanup function
  return () => {
    clearInterval(timer);
    console.log("Cleanup: Timer stopped");
  };
}, []);

When this component unmounts, React automatically calls the function returned inside the effect, effectively preventing the setInterval from running indefinitely in the background.

Handling Pending Operations: A Concrete Example

In our movie-browser project, we often fetch data. If a user quickly toggles between movie categories, we might have multiple fetch requests flying at once. If an older request finishes after a newer one, it could overwrite the state with outdated information—this is a classic race condition.

Here is how you handle it using a boolean flag:

JAVASCRIPT
useEffect(() => {
  let isCancelled = false;

  const fetchData = async () => {
    const response = await fetch(CE9178">`https://api.example.com/movies/${category}`);
    const data = await response.json();

    // Only update state if the component hasn't unmounted/changed
    if (!isCancelled) {
      setMovies(data);
    }
  };

  fetchData();

  // Cleanup: flag the request as cancelled
  return () => {
    isCancelled = true;
  };
}, [category]);

By setting isCancelled to true inside the cleanup function, we ensure that even if the fetch promise finally resolves, the setMovies state update will be ignored.

Hands-on Exercise: Clean Up Your Timer

In your current movie browser, I want you to add a "Live Clock" feature to the header.

  1. Create a Clock component.
  2. Use useState to store the current time and useEffect to update it every second using setInterval.
  3. Crucial: Implement the cleanup function using clearInterval.
  4. Observe the console logs: verify that "Cleanup" runs when you remove the component from the screen.

Common Pitfalls

  • Forgetting to Cleanup: The most common issue is leaving setTimeout or setInterval running. This is a primary cause of memory leaks in production apps.
  • Assuming Cleanup Runs on Every Render: Remember that the cleanup function only runs when the component unmounts OR when the dependencies change. It does not run on every single render.
  • Stale Closures: Since the cleanup function is a closure, it captures the variables from the scope where it was created. Always ensure your cleanup logic doesn't rely on state values that might have changed unexpectedly.

Recap

Effective side-effect management is what separates a prototype from a robust application. By mastering the cleanup function in useEffect, you prevent memory leaks, stop stale state updates, and ensure your component lifecycle remains predictable. Always ask yourself: "If this component disappears right now, what needs to stop running?"

Up next: We'll learn how to implement debouncing for our search input to minimize API traffic and further optimize our app's performance.

Similar Posts