Not handling cleanup inside useEffect when dealing with subscriptions

Loading

Handling Cleanup in useEffect for Subscriptions

A critical but often overlooked aspect of React’s useEffect hook is proper cleanup of subscriptions and side effects. Failing to do this can lead to memory leaks, performance issues, and unexpected behavior in your application.

The Problem: Memory Leaks from Unclosed Subscriptions

When you don’t clean up effects:

useEffect(() => {
  const subscription = dataStream.subscribe(data => {
    setData(data);
  });
  // ❌ Missing cleanup - subscription remains after component unmounts
}, []);

Consequences:

  • Memory leaks (subscriptions keep holding references)
  • Attempts to update state in unmounted components
  • Performance degradation
  • Potential duplicate subscriptions on re-mount

The Solution: Cleanup Function

Every useEffect can return a cleanup function:

useEffect(() => {
  const subscription = dataStream.subscribe(data => {
    setData(data);
  });

  // ✅ Cleanup function
  return () => {
    subscription.unsubscribe();
  };
}, []);

Common Scenarios Requiring Cleanup

1. Event Listeners

useEffect(() => {
  const handleResize = () => {
    setWidth(window.innerWidth);
  };

  window.addEventListener('resize', handleResize);

  return () => {
    window.removeEventListener('resize', handleResize);
  };
}, []);

2. API Subscriptions

useEffect(() => {
  const socket = new WebSocket('wss://api.example.com');

  socket.onmessage = (event) => {
    setMessages(prev => [...prev, event.data]);
  };

  return () => {
    socket.close(); // ✅ Clean up WebSocket
  };
}, []);

3. Timers and Intervals

useEffect(() => {
  const timer = setInterval(() => {
    setSeconds(prev => prev + 1);
  }, 1000);

  return () => {
    clearInterval(timer); // ✅
  };
}, []);

4. Asynchronous Operations

useEffect(() => {
  let isMounted = true; // Flag to track mounted state

  fetchData().then(data => {
    if (isMounted) { // Only update if component is mounted
      setData(data);
    }
  });

  return () => {
    isMounted = false; // ✅ Cleanup by setting flag
  };
}, []);

Advanced Patterns

1. Combining Multiple Cleanups

useEffect(() => {
  const subscription = dataStream.subscribe(handleData);
  const timer = setInterval(pollData, 5000);

  return () => {
    subscription.unsubscribe();
    clearInterval(timer);
  };
}, [handleData]);

2. Cleanup with Dependencies

useEffect(() => {
  const controller = new AbortController();

  fetch(url, { signal: controller.signal })
    .then(response => response.json())
    .then(setData);

  return () => {
    controller.abort(); // Aborts fetch when url changes or unmounts
  };
}, [url]); // ✅ Cleanup runs when url changes

Common Mistakes

Forgetting cleanup for event listeners

useEffect(() => {
  document.addEventListener('keydown', handleKeyPress);
  // ❌ Missing cleanup
}, [handleKeyPress]);

Assuming cleanup isn’t needed for API calls

useEffect(() => {
  fetch(url).then(setData);
  // ❌ Should abort if component unmounts
}, [url]);

Correct Approach

useEffect(() => {
  let isActive = true;

  fetch(url)
    .then(response => isActive && setData(response));

  return () => {
    isActive = false;
  };
}, [url]);

Real-World Example

Chat Component with Cleanup

function ChatRoom({ roomId }) {
  const [messages, setMessages] = useState([]);

  useEffect(() => {
    const connection = createConnection(roomId);
    connection.connect();

    connection.on('message', (msg) => {
      setMessages(msgs => [...msgs, msg]);
    });

    return () => {
      connection.disconnect(); // ✅ Clean up when roomId changes or unmounts
    };
  }, [roomId]); // Effect reruns when roomId changes

  return <MessageList messages={messages} />;
}

Why Cleanup Runs on Every Dependency Change

React performs cleanup:

  1. Before re-running the effect (when dependencies change)
  2. When the component unmounts

This ensures no stale subscriptions or side effects remain.

Key Takeaways

  1. Always return a cleanup function from useEffect for subscriptions
  2. Clean up:
  • Event listeners
  • WebSocket/subscription connections
  • Timers/intervals
  • Async operations
  1. Use flags (isMounted) for async operations
  2. Combine cleanups when multiple resources need disposal
  3. Remember cleanup runs before re-execution and on unmount

Proper cleanup ensures your React components manage resources efficiently and avoid memory leaks throughout their lifecycle.

Leave a Reply

Your email address will not be published. Required fields are marked *