Search code examples
reactjsfetchuse-effect

custom file for api calls


import { useState, useCallback, useRef, useEffect } from 'react';

export const useHttpClient = () => {
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState();

  const activeHttpRequests = useRef([]);

  const sendRequest = useCallback(
    async (url, method = 'GET', body = null, headers = {}) => {
      setIsLoading(true);
      const httpAbortCtrl = new AbortController();
      activeHttpRequests.current.push(httpAbortCtrl);

      try {
        const response = await fetch(url, {
          method,
          body,
          headers,
          signal: httpAbortCtrl.signal
        });

        const responseData = await response.json();

        activeHttpRequests.current = activeHttpRequests.current.filter(
          reqCtrl => reqCtrl !== httpAbortCtrl
        );

        if (!response.ok) {
          throw new Error(responseData.message);
        }

        setIsLoading(false);
        return responseData;
      } catch (err) {
        setError(err.message);
        setIsLoading(false);
        throw err;
      }
    }, []);

  const clearError = () => {
    setError(null);
  };

  useEffect(() => {
    return () => {
      // eslint-disable-next-line react-hooks/exhaustive-deps
      activeHttpRequests.current.forEach(abortCtrl => abortCtrl.abort());
    };
  }, []);

  return { isLoading, error, sendRequest, clearError };
};

Usage:

 const fetchUsers = async () => {
      try {
        const responseData = await sendRequest(
          'http://localhost:5000/api/users?page=1'
        );

        setLoadedUsers(responseData);
      } catch (err) {}
  };

Currently this is code that i got, i want to make it simplier so i dont need to write on every fetch the cleaning up functions. In this file it used controller and theres also passed abourt on end on useeffect but times when i start to switching pages really fast and dont even give server to load content it consoling me log for unmounted error.. Is there anyone that can help me about this? Maybe theres something wrong in this code or something?


Solution

  • The abort controller rejects only the fetch promise, it doesn't affect any others. Moreover, you're trying to change the state in the catch block without any check whether the component was unmounted or not. You should do these checks manually. The weird array of abort controllers is unnecessary, you can use one controller for all requests. This code is ugly, but it just illustrates the approach... (Live demo)

    export default function TestComponent(props) {
      const [isLoading, setIsLoading] = useState(false);
      const [error, setError] = useState();
      const [text, setText] = useState("");
    
      const controllerRef = useRef(null);
      const isMountedRef = useRef(true);
    
      const sendRequest = useCallback(
        async (url, method = "GET", body = null, headers = {}) => {
          setIsLoading(true);
          const httpAbortCtrl =
            controllerRef.current ||
            (controllerRef.current = new AbortController());
    
          try {
            const response = await fetch(url, {
              method,
              body,
              headers,
              signal: httpAbortCtrl.signal
            });
    
            //if (!isMountedRef.current) return; // nice to have this here
    
            const responseData = await response.json();
    
            if (!response.ok) {
              throw new Error(responseData.message);
            }
            if (!isMountedRef.current) return;
            setIsLoading(false);
            return responseData;
          } catch (err) {
            if (isMountedRef.current) {
              setError(err.message);
              setIsLoading(false);
            }
            throw err;
          }
        },
        []
      );
    
      useEffect(() => {
        return () => {
          isMountedRef.current = false;
          controllerRef.current && controllerRef.current.abort();
        };
      }, []);
    
      const fetchUsers = async () => {
        try {
          const responseData = await sendRequest(props.url);
    
          isMountedRef.current && setText(JSON.stringify(responseData));
        } catch (err) {}
      };
    
      return (
        <div className="component">
          <div className="caption">useAsyncEffect demo:</div>
          <div>{error ? error.toString() : text}</div>
          <button onClick={fetchUsers}>Send request</button>
        </div>
      );
    }
    

    You can use a custom library for fetching, which automatically cancels async code (and aborts the request) when the related component unmounting. You can use it directly in your components. So behold the magic :) The simplest demo ever :

    import React, { useState } from "react";
    import { useAsyncEffect } from "use-async-effect2";
    import cpAxios from "cp-axios";
    
    export default function TestComponent(props) {
      const [text, setText] = useState("");
    
      const cancel = useAsyncEffect(
        function* () {
          const response = yield cpAxios(props.url);
          setText(JSON.stringify(response.data));
        },
        [props.url]
      );
    
      return (
        <div className="component">
          <div className="caption">useAsyncEffect demo:</div>
          <div>{text}</div>
          <button onClick={cancel}>Cancel request</button>
        </div>
      );
    }
    

    Or a more practical example with fetching error handling (Live Demo):

    import React, { useState } from "react";
    import { useAsyncCallback, E_REASON_UNMOUNTED } from "use-async-effect2";
    import { CanceledError } from "c-promise2";
    import cpAxios from "cp-axios";
    
    export default function TestComponent(props) {
      const [text, setText] = useState("");
      const [isFetching, setIsFetching] = useState(false);
    
      const fetchUrl = useAsyncCallback(
        function* (options) {
          try {
            setIsFetching(true);
            setText("fetching...");
            const response = yield cpAxios(options).timeout(props.timeout);
            setText(JSON.stringify(response.data));
            setIsFetching(false);
          } catch (err) {
            CanceledError.rethrow(err, E_REASON_UNMOUNTED);
            setText(err.toString());
            setIsFetching(false);
          }
        },
        [props.url]
      );
    
      return (
        <div className="component">
          <div className="caption">useAsyncEffect demo:</div>
          <div>{text}</div>
          <button onClick={() => fetchUrl(props.url)} disabled={isFetching}>
            Fetch data
          </button>
          <button onClick={() => fetchUrl.cancel()} disabled={!isFetching}>
            Cancel request
          </button>
        </div>
      );
    }