Loading

Quipoin Menu

Learn • Practice • Grow

react / Cleanup in useEffect
tutorial

Cleanup in useEffect

Imagine you're listening to music with headphones. When you're done, you unplug the headphones. If you don't, they might drain your device's battery or cause audio to play through the wrong speakers. In React, side effects often need similar cleanup to prevent memory leaks and unexpected behavior.

What is Cleanup in useEffect?

The `useEffect` hook can return a function. This function is called the cleanup function, and React will run it right before the component unmounts or before re-running the effect due to dependency changes.

Think of cleanup as the "opposite" of your effect. If you set something up (like a timer or subscription), cleanup tears it down.

When Does Cleanup Run?

The cleanup function runs in two scenarios:
  1. Before the component unmounts: When the component is removed from the DOM.
  2. Before re-running the effect: When dependencies change, React runs the cleanup from the previous render first, then runs the new effect.

Why Do We Need Cleanup?

  • Prevent Memory Leaks: Timers, subscriptions, and event listeners continue running even after the component is gone, wasting resources.
  • Avoid Race Conditions: When doing async operations (like API calls), cleanup can prevent setting state on an unmounted component.
  • Maintain Consistency: Ensure that side effects don't interfere with each other across renders.

Example 1: Cleaning Up Timers
function Timer() {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    console.log('Setting up timer...');
    const intervalId = setInterval(() => {
      setSeconds(prev => prev + 1);
    }, 1000);

    <!-- CLEANUP FUNCTION -->
    return () => {
      console.log('Cleaning up timer...');
      clearInterval(intervalId);
    };
  }, []); <!-- Empty array = run once on mount -->

  return <div>Seconds: {seconds}</div>;
}

Without cleanup, the timer would keep running even if the `Timer` component is removed from the page, causing a memory leak.

Example 2: Cleaning Up Event Listeners
function MouseTracker() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    console.log('Adding mousemove listener...');
    function handleMove(event) {
      setPosition({ x: event.clientX, y: event.clientY });
    }
    window.addEventListener('mousemove', handleMove);

    <!-- CLEANUP: Remove event listener -->
    return () => {
      console.log('Removing mousemove listener...');
      window.removeEventListener('mousemove', handleMove);
    };
  }, []);

  return (
    <div>
      Mouse position: ({position.x}, {position.y})
    </div>
  );
}

Example 3: Preventing Race Conditions in Async Code
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    let isActive = true; <!-- Flag to prevent setting state after cleanup -->
    fetch(`https://api.example.com/users/${userId}`)
      .then(response => response.json())
      .then(data => {
        if (isActive) {
          setUser(data);
        } else {
          console.log('Component unmounted, ignoring result');
        }
      });

    <!-- Cleanup sets isActive to false -->
    return () => {
      isActive = false;
    };
  }, [userId]);

  return <!-- render user profile -->;
}

This pattern is crucial when fetching data. If the user navigates away quickly, the API call might complete later and try to set state on an unmounted component, causing a warning. The cleanup flag prevents this.

Example 4: Cleanup on Dependency Change
function ChatRoom({ roomId }) {
  useEffect(() => {
    console.log('Connecting to room', roomId);
    const connection = createConnection(roomId);
    connection.connect();

    return () => {
      console.log('Disconnecting from room', roomId);
      connection.disconnect();
    };
  }, [roomId]); <!-- Cleanup runs before re-connecting when roomId changes -->
}

When `roomId` changes from 'general' to 'sports', the cleanup runs (disconnecting from 'general'), then the new effect runs (connecting to 'sports').

Two Minute Drill

  • `useEffect` can return a cleanup function that runs before unmount or before re-running the effect.
  • Cleanup is essential for preventing memory leaks from timers, subscriptions, and event listeners.
  • Use cleanup to cancel pending async operations and prevent setting state on unmounted components.
  • When dependencies change, cleanup runs with the old values, then the effect runs with the new values.
  • Common cleanup tasks: `clearInterval`, `clearTimeout`, `removeEventListener`, `unsubscribe`, aborting fetch requests.
  • Always clean up what you set up – it's a fundamental principle of robust React applications.

Need more clarification?

Drop us an email at career@quipoinfotech.com