Home > Mobile >  React custom Axios hook with processing state, causes infinite loop
React custom Axios hook with processing state, causes infinite loop

Time:01-15

I'm trying to create a custom Axios hook, with processing/loading state. I mean the hook should return an axios instance and also a processing state, so that I can use that state to show some spinner or disable submit button etc.

I've used Axios interceptors for it, like this:

const useAxios = ({
  baseURL = process.env.NEXT_PUBLIC_BACKEND_URL,
  withCredentials = true,
  headers = {
    'Content-Type': 'application/json',
    'Accept': 'application/json'
  }
}) => {
  const [processing, setProcessing] = useState(false)

  const instance = axios.create({
    baseURL: baseURL,
    headers: headers,
    withCredentials,
  });

  instance.interceptors.request.use(
    function (config) {
      setProcessing(true)
      return config;
    },
    function (error) {
      setProcessing(false)
      return Promise.reject(error);
    }
  );

  instance.interceptors.response.use(
    function (response) {
      setProcessing(false)
      return response;
    },
    function (error) {
      setProcessing(false)
      return Promise.reject(error);
    }
  );

  return {
    axios: instance,
    processing,
  }
}

The problem:

When I'm trying to use this hook inside some component's useEffect like this:

  const { axios } = useAxios();

  useEffect(() => {
    axios.get('/api/some-endpoint')
  }, [axios])

It causes infinite loop, I think the issue here is, as the useAxios has a state (processing) so, when-ever that state changes, the useEffect runs and that useEffect again calls the API which again causes the state to update, and we get the loop.

If I simply remove the axios from useEffect's dependecy array it works fine, But eslint is not happy with that, it gives this error: React Hook useEffect has a missing dependency: 'axios'. Either include it or remove the dependency array. eslintreact-hooks/exhaustive-deps So, I guess that's not a good practice.

I'm not sure, what to do exactly in this case.

CodePudding user response:

Your instance needs to be wrapped in a useCallback to ensure that it does not cause components to re-trigger since you're using set processing to cause a re-render to the hook which causes the instance to change which would then cause an infinite loop in your consuming component.

Since the instance does not need to change, you can wrap the instance around a React.useCallback to ensure that when you're changing the processing value, it will no longer an infinite loop.

const useAxios = ({
  baseURL = process.env.NEXT_PUBLIC_BACKEND_URL,
  withCredentials = true,
  headers = {
    'Content-Type': 'application/json',
    'Accept': 'application/json'
  }
}) => {
  const [processing, setProcessing] = useState(false)

  const instance = React.useCallback(() => {
    axios.create({
      baseURL: baseURL,
      headers: headers,
      withCredentials,
    });

    instance.interceptors.request.use(
      function (config) {
        setProcessing(true)
        return config;
      },
      function (error) {
        setProcessing(false)
        return Promise.reject(error);
      }
    );

    instance.interceptors.response.use(
      function (response) {
        setProcessing(false)
        return response;
      },
      function (error) {
        setProcessing(false)
        return Promise.reject(error);
      }
    );
  }, [baseURL, headers, withCredentials]);

  return {
    axios: instance,
    processing,
  }
}

CodePudding user response:

You are recreating the axios instance on render, the change triggers the useEffect, which makes an api call, that causes a re-render, that changes the axios instance...

Memoize the Axios instance using a ref and useMemo, and make sure that all dependencies won't change between renders (like the defaultHeaders), and memoize them as well:

const defaultHeaders = {
  'Content-Type': 'application/json',
  'Accept': 'application/json'
}

const useAxios = ({
  baseURL = process.env.NEXT_PUBLIC_BACKEND_URL,
  withCredentials = true,
  headers = defaultHeaders
}) => {
  const axiosRef = useRef()
  const [processing, setProcessing] = useState(false)

  useEffect(() => {
    const instance = axios.create({
      baseURL: baseURL,
      headers: headers,
      withCredentials,
    });

    instance.interceptors.request.use(
      function(config) {
        setProcessing(true)
        return config;
      },
      function(error) {
        setProcessing(false)
        return Promise.reject(error);
      }
    );

    instance.interceptors.response.use(
      function(response) {
        setProcessing(false)
        return response;
      },
      function(error) {
        setProcessing(false)
        return Promise.reject(error);
      }
    );

    axiosRef.current = instance;
  }, [baseURL, withCredentials, headers])

  return {
    axios: axiosRef.current,
    processing,
  }
}
  • Related