← Back to React Course | Chapter 5: Core Hooks | Lesson 3 of 12

Cleanup Functions in useEffect

A cleanup function is useEffect tidying up after itself before it does its next job, like turning off the stove before starting a new recipe.

Returning a Cleanup Function

If the function passed to useEffect returns another function, React treats that returned function as cleanup logic. React calls it right before running the effect again, and one final time when the component is removed from the page.

Note: Cleanup functions are especially important for anything that keeps running in the background, like setInterval or a WebSocket connection.

Warning: Forgetting the cleanup function for a setInterval effect means the interval keeps running even after the component is gone, wasting resources.

Example: Returning a Cleanup Function

markup
<!DOCTYPE html>
<html>
<head>
  <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
  <div id="root"></div>
  <script type="text/babel">
    function Timer() {
      const [seconds, setSeconds] = React.useState(0);
      React.useEffect(() => {
        const id = setInterval(() => setSeconds(s => s + 1), 1000);
        return () => clearInterval(id);
      }, []);
      return <p>Seconds: {seconds}</p>;
    }
    ReactDOM.createRoot(document.getElementById('root')).render(<Timer />);
  </script>
</body>
</html>

Cleanup Runs Before Re-running the Effect

Whenever an effect's dependencies change and it's about to run again, React first calls the previous cleanup function. This ensures old subscriptions or listeners are properly removed before new ones are set up, preventing duplicates from piling up.

Note: Think of cleanup as always pairing with setup -- if the effect subscribes to something, the cleanup should unsubscribe from that same thing.

Warning: Without cleanup, an effect that adds an event listener on every dependency change will keep stacking up duplicate listeners.

Example: Cleanup Runs Before Re-running the Effect

markup
<!DOCTYPE html>
<html>
<head>
  <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
  <div id="root"></div>
  <script type="text/babel">
    function KeyLogger() {
      const [key, setKey] = React.useState("");
      React.useEffect(() => {
        function handleKey(e) { setKey(e.key); }
        window.addEventListener("keydown", handleKey);
        return () => window.removeEventListener("keydown", handleKey);
      }, []);
      return <p>Last key pressed: {key || "(none yet)"}</p>;
    }
    ReactDOM.createRoot(document.getElementById('root')).render(<KeyLogger />);
  </script>
</body>
</html>

Cleanup on Unmount

When a component is removed from the page entirely, React calls its effect's cleanup function one final time. This is the standard place to release any resources the component was using, so nothing keeps running after the component is gone.

Note: If your component sets something up that should stop existing once the component disappears, that logic belongs in the cleanup function.

Warning: A component that toggles on and off repeatedly (via conditional rendering) mounts and unmounts each time, running setup and cleanup every time too.

Example: Cleanup on Unmount

markup
<!DOCTYPE html>
<html>
<head>
  <script src="https://unpkg.com/react@18/umd/react.development.js"></script>
  <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
  <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
  <div id="root"></div>
  <script type="text/babel">
    function Blinker() {
      React.useEffect(() => {
        console.log("Blinker mounted");
        return () => console.log("Blinker unmounted -- cleanup ran");
      }, []);
      return <p>I blink on and off (see console on toggle).</p>;
    }
    function App() {
      const [show, setShow] = React.useState(true);
      return (
        <div>
          <button onClick={() => setShow(!show)}>Toggle</button>
          {show && <Blinker />}
        </div>
      );
    }
    ReactDOM.createRoot(document.getElementById('root')).render(<App />);
  </script>
</body>
</html>
Common Mistakes
  1. Forgetting to return a cleanup function for effects that set up subscriptions, timers, or event listeners, causing memory leaks.
  2. Returning something other than a function (like a value or nothing when a function was needed) from the effect.
  3. Assuming cleanup only runs when the component unmounts -- it also runs before every re-run of the effect itself.
Chapter Summary
  • A cleanup function is returned from inside the effect function.
  • React calls it before the effect runs again, and when the component unmounts.
  • Cleanup is essential for timers, subscriptions, and event listeners to avoid leaks.
  • Not every effect needs a cleanup function -- only ones that set up something ongoing.
Browser Support

No browser-specific restrictions -- cleanup behavior is consistent since Hooks' introduction in React 16.8.

Login to run this code

C/C++/Java/PHP execution requires a free account. Your code is saved — you'll land right back in the editor after logging in.