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 AbortError
s 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: });