React Custom useRequest Hook causes MemoryLeak

I’m working on a project that uses a custom hook useRequest to fetch data from server.

The implementation is

const useRequest = ({ url }) => {
  const [isLoading, setIsLoading] = React.useState(true);
  const [error, setError] = React.useState(null);
  const [apiData, setApiData] = React.useState({});

  React.useEffect(() => {
    const abortController = new AbortController();

    fetch(url, {
      headers: headers,
      signal: abortController.signal,
    })
      .then((res) => res.json())
      .then((data) => {
        setIsLoading(false);
        if (data.status_message) {
          throw Error(data.status_message);
        }
        setApiData(data);
      })
      .catch((err) => {
        setError(err);
        setIsLoading(false);
      });

    return function cleanup() {
      abortController.abort();
    };
  }, [url]);
  return { isLoading, apiData, error };
};

export default useRequest;




I don’t know why but sometimes it throws memory-leak error. Can someone help why it is so?

Components where I’m using the useRequest hook:

1) ItemInfo Component-

function Item() {
  const { id } = useParams();
  const { path } = useRouteMatch();
  const type = path.split("/")[1];

  const { isLoading, apiData, error } = useRequest({
    url: request.getItemData(id, type),
  });
  if (error) return <NotFound error={error} />;

  if (isLoading) return <Loader />;
  const RowComp = withRow(Row);

  return (
    <div className="itemInfo">
      {apiData && (
        <>
          <Banner data={apiData} />
          <div className="itemInfo__wrapper">
            <div className="itemInfo__overview">
              <h2>Overview</h2>
              <p>{apiData.overview}</p>
            </div>
            <Details details={apiData} />
            <RowComp title="Cast" url={request.getCast(type, id)} />
            {type === "movie" && apiData.belongs_to_collection && (
              <RowComp
                url={request.getCollection(apiData.belongs_to_collection.id)}
                title="Collections"
              />
            )}
            {type === "tv" && apiData.seasons && (
              <Row results={apiData.seasons} title="Seasons" />
            )}
            <RowComp
              url={request.getRecommendations(id, type)}
              title="Recommendations"
            />
          </div>
        </>
      )}
    </div>
  );
}

export default Item;

2) An HOC

function withRow(WrappedComponent) {
  const HOC = ({ url, title }) => {
    const { isLoading, apiData } = useRequest({ url });
    const results = apiData.results || apiData.parts || apiData.cast;
    return (
      <>
        {isLoading && <Loader />}
        {results && <WrappedComponent results={results} title={title} />}
      </>
    );
  };
  HOC.propTypes = {
    url: PropTypes.string,
    title: PropTypes.string,
  };
  return HOC;
}

The HOC component mainly logs the error but sometime ItemInfo Component also logs Memory Leak error Thanks in advance.

Answer

Here’s what happens:

1.) The components unmount.

2.) Effect cleanups happen (directly afterwards, synchronously), and abortController.abort(); gets called. From now on no setState shall be called on the component.

3.) Either fetch(...) or res.json() (?) throw the exception, and the Promise gets rejected

4.) In a microtask, the .catch callback runs, and setError and setIsLoading get called. As the component does not exist anymore, the error you see is raised.

In my eyes cleaning up the fetch should be silent, so AbortErrors should be supressed:

.catch(error => {
   // If the component is cleaned up, and the fetch() is aborted, silently stop as nobody is listening to our error anyways
   if(error.name === "ABORT_ERR") return;
   // In other cases, raise error to component:
});

Leave a Reply

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