reactjsaxiosreact-hooks

Canceling an Axios REST call in React Hooks useEffects cleanup failing


I'm obviously not cleaning up correctly and cancelling the axios GET request the way I should be. On my local, I get a warning that says

Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.

On stackblitz, my code works, but for some reason I can't click the button to show the error. It just always shows the returned data.

https://codesandbox.io/s/8x5lzjmwl8

Please review my code and find my flaw.

useAxiosFetch.js

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

const useAxiosFetch = url => {
    const [data, setData] = useState(null)
    const [error, setError] = useState(null)
    const [loading, setLoading] = useState(true)

    let source = axios.CancelToken.source()
    useEffect(() => {
        try {
            setLoading(true)
            const promise = axios
                .get(url, {
                    cancelToken: source.token,
                })
                .catch(function (thrown) {
                    if (axios.isCancel(thrown)) {
                        console.log(`request cancelled:${thrown.message}`)
                    } else {
                        console.log('another error happened')
                    }
                })
                .then(a => {
                    setData(a)
                    setLoading(false)
                })
        } catch (e) {
            setData(null)
            setError(e)
        }

        if (source) {
            console.log('source defined')
        } else {
            console.log('source NOT defined')
        }

        return function () {
            console.log('cleanup of useAxiosFetch called')
            if (source) {
                console.log('source in cleanup exists')
            } else {
                source.log('source in cleanup DOES NOT exist')
            }
            source.cancel('Cancelling in cleanup')
        }
    }, [])

    return {data, loading, error}
}

export default useAxiosFetch

index.js

import React from 'react';

import useAxiosFetch from './useAxiosFetch1';

const index = () => {
    const url = "http://www.fakeresponse.com/api/?sleep=5&data={%22Hello%22:%22World%22}";
    const {data,loading} = useAxiosFetch(url);

    if (loading) {
        return (
            <div>Loading...<br/>
                <button onClick={() => {
                    window.location = "/awayfrom here";
                }} >switch away</button>
            </div>
        );
    } else {
        return <div>{JSON.stringify(data)}xx</div>
    }
};

export default index;

Solution

  • Here is the final code with everything working in case someone else comes back.

    import {useState, useEffect} from "react";
    import axios, {AxiosResponse} from "axios";
    
    const useAxiosFetch = (url: string, timeout?: number) => {
        const [data, setData] = useState<AxiosResponse | null>(null);
        const [error, setError] = useState(false);
        const [errorMessage, setErrorMessage] = useState(null);
        const [loading, setLoading] = useState(true);
    
        useEffect(() => {
            let unmounted = false;
            let source = axios.CancelToken.source();
            axios.get(url, {
                cancelToken: source.token,
                timeout: timeout
            })
                .then(a => {
                    if (!unmounted) {
                        // @ts-ignore
                        setData(a.data);
                        setLoading(false);
                    }
                }).catch(function (e) {
                if (!unmounted) {
                    setError(true);
                    setErrorMessage(e.message);
                    setLoading(false);
                    if (axios.isCancel(e)) {
                        console.log(`request cancelled:${e.message}`);
                    } else {
                        console.log("another error happened:" + e.message);
                    }
                }
            });
            return function () {
                unmounted = true;
                source.cancel("Cancelling in cleanup");
            };
        }, [url, timeout]);
    
        return {data, loading, error, errorMessage};
    };
    
    export default useAxiosFetch;