1
votes

I have a custom hook that I'm using to make API requests on my react front-end application but the hook seems to be having a bug.

It makes API requests as intended but whenever I unmount the current container/page in which the request is being made, my hook doesn't know that the page has been unmounted so it doesn't cancel the request and therefore react throws the 'Can't perform a React state update on an unmounted component' warning.

export function useFetch(initialValue, url, options, key) {
  const [response, setResponse] = useLocalStorage(key, initialValue);
  const [error, setError] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    const CancelToken = axios.CancelToken;
    const source = CancelToken.source();
    const isMounted = { state: true };

    async function fetchData() {
      setLoading(true);
      try {
        const res = await axios({
          url: url,
          baseURL: BASE_URL,
          cancelToken: source.token,
          ...options
        });
        if (res.data.results) {
          setResponse(res.data.results);
        } else {
          setResponse(res.data);
        }
        setLoading(false);
      } catch (error) {
        setError(error);
        setLoading(false);
      }
    }
    if (isMounted.state) {
      fetchData();
    }

    return () => {
      isMounted.state = false;
      source.cancel('Operation canceled by the user.');
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [url]);
  return [response, { error, loading }];
}

2

2 Answers

0
votes

By now you are checking for if(isMounter.state) in wrong place. It's currently very next step after you've initialized it.

I believe it should be

    const isMounted = { state: true };

    async function fetchData() {
      setLoading(true);
      try {
        const res = await axios({
          url: url,
          baseURL: BASE_URL,
          cancelToken: source.token,
          ...options
        });
        if(!isMounted.state) return;
        .....
      }
    }
    fetchData();

BTW you don't have to use object there: isMounted = true/isMounted = false will work just fine through closure.

Actually your have 2 different approaches mixed: using flag(isMounted) and cancelling request. You may use just one. Cancelling request should work(as far as I see) but it leads your catch block is executed:

} catch (error) {
  setError(error);
  setLoading(false);
}

See, unmounting cancels request, but your code still tries to set up some state. Probably you better check if request has been failed or canceled with axious.isCancel:

} catch (error) {
  if (!axios.isCancel(error)) {
    setError(error);
    setLoading(false);
  }
}

And you may get rid of isMounted in this case.

0
votes

I use the following hook to get an ifMounted function

const useIfMounted = () => {
    const isMounted = useRef(true)
    useEffect(
      () => () => {
        isMounted.current = false
      },[]
    )

    const ifMounted = useCallback(
      func => {
        if (isMounted.current && func) {
          func()
        } 
      },[]
    )

    return ifMounted
}

Then in your code add const ifMounted = useIfMounted() to useFetch and before your set functions do ifMounted(() => setLoading(true), ifMounted(() => setError(error)), etc....

Here's a blog post I wrote on the subject: https://aceluby.github.io/blog/react-hooks-cant-set-state-on-an-unmounted-component