Not canceling API requests on unmount leading to memory leaks

Loading

Not Canceling API Requests on Unmount Leading to Memory Leaks

When working with API calls in React, especially inside useEffect, failing to cancel ongoing requests when the component unmounts can lead to memory leaks. This is particularly problematic when the API response updates the state after the component is no longer in the DOM.


Common Mistake Example (Missing Cleanup):

import { useEffect, useState } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch(`https://api.example.com/user/${userId}`)
      .then(res => res.json())
      .then(data => setUser(data)); // ❌ No cleanup, potential memory leak

    // No cleanup function provided
  }, [userId]);

  return <h1>{user ? user.name : 'Loading...'}</h1>;
}

Why This Causes a Memory Leak:

  • If the component unmounts before the API call completes, React tries to update the state of an unmounted component.
  • This can cause warnings in React and lead to unnecessary memory usage.

Correct Way (Using AbortController for Cleanup):

import { useEffect, useState } from 'react';

function UserProfile({ userId }) {
  const [user, setUser] = useState(null);

  useEffect(() => {
    const controller = new AbortController(); // ✅ Create an AbortController
    const signal = controller.signal;

    fetch(`https://api.example.com/user/${userId}`, { signal })
      .then(res => res.json())
      .then(data => setUser(data))
      .catch(err => {
        if (err.name === 'AbortError') {
          console.log('Fetch aborted'); // ✅ Handle abort error gracefully
        } else {
          console.error(err);
        }
      });

    return () => {
      controller.abort(); // ✅ Cleanup: cancel request on unmount
    };
  }, [userId]);

  return <h1>{user ? user.name : 'Loading...'}</h1>;
}

Why This Works:

  • AbortController allows you to cancel the request when the component unmounts.
  • The cleanup function inside useEffect ensures the API request is canceled, preventing memory leaks.

Common Mistake: Not Handling Errors Correctly

import { useEffect, useState } from 'react';

function Posts() {
  const [posts, setPosts] = useState([]);

  useEffect(() => {
    fetch('https://api.example.com/posts')
      .then(res => res.json())
      .then(data => setPosts(data))
      .catch(err => console.error(err)); // ❌ No handling for aborted fetch
  }, []);

  return (
    <ul>
      {posts.map(post => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

Fix:

  • Always handle the AbortError to prevent unnecessary error logs.
  • Use AbortController for proper cleanup.

Using AbortController with Async/Await

import { useEffect, useState } from 'react';

function Comments({ postId }) {
  const [comments, setComments] = useState([]);

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

    const fetchComments = async () => {
      try {
        const response = await fetch(`https://api.example.com/comments/${postId}`, { signal });
        const data = await response.json();
        setComments(data);
      } catch (err) {
        if (err.name === 'AbortError') {
          console.log('Request was aborted');
        } else {
          console.error(err);
        }
      }
    };

    fetchComments();

    return () => {
      controller.abort(); // ✅ Cancel the request on unmount
    };
  }, [postId]);

  return (
    <ul>
      {comments.map(comment => (
        <li key={comment.id}>{comment.text}</li>
      ))}
    </ul>
  );
}

Why This Works:

  • The async function handles both data fetching and error handling.
  • The request is aborted if the component unmounts, preventing memory leaks.

Mistake: Forgetting to Cleanup with Third-Party Libraries

If you’re using libraries like axios, they don’t support AbortController directly. In such cases, use the CancelToken:

import axios from 'axios';
import { useEffect, useState } from 'react';

function DataFetcher() {
  const [data, setData] = useState(null);

  useEffect(() => {
    const cancelTokenSource = axios.CancelToken.source();

    axios.get('https://api.example.com/data', {
      cancelToken: cancelTokenSource.token,
    })
      .then(response => setData(response.data))
      .catch(error => {
        if (axios.isCancel(error)) {
          console.log('Request canceled:', error.message);
        } else {
          console.error(error);
        }
      });

    return () => {
      cancelTokenSource.cancel('Operation canceled by the user.'); // ✅ Cleanup
    };
  }, []);

  return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;
}

Key Takeaways:

  • Always clean up API calls in useEffect using AbortController or CancelToken.
  • Handle errors gracefully, especially for canceled requests.
  • This improves performance, prevents memory leaks, and avoids React warnings.

Leave a Reply

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