React Hooks: Display global spinner using axios interceptor?
Asked Answered
I

1

18

I would like to add a Loader component to be rendered whenever an API call is being made in React. I want to use react context + hooks instead of redux.

As the rules of react hooks say, we should not use react hooks outside the react component. But I need to dispatch the SHOW_LOADER and HIDE_LOADER inside the Axios interceptor as below.

Is there a way to achieve this?

import axios from "axios";
axios.interceptors.request.use(
  config => {
    dispatch({
    type: "SHOW_LOADER"
})
    return config;
  },
  error => {
     dispatch({
    type: "HIDE_LOADER"
})
    return Promise.reject(error);
  }
);

axios.interceptors.response.use(
  response => {
    dispatch({
    type: "HIDE_LOADER"
})
    return response;
  },
  error => {
    dispatch({
    type: "HIDE_LOADER"
})
    return Promise.reject(error);
  }
);
function GlobalLoader(){
    const [state,dispatch] = useContext(LoaderContext);
    return(
        <div>
            {
                state.loadStatus &&
                    <Loader
                    type = "Puff"
                    color = "#00BFFF"
                    height = {100}
                    width = {100}
                    timeout = {3000} />
            }
        </div>
    );
}

export default GlobalLoader;

Please let me know if more information is required.:)

Incomer answered 14/12, 2019 at 14:19 Comment(4)
why not move the interceptors to inside a useEffect in GlobalLoader?Iquique
useEffect will not be executed when we make an API request in the child component.Incomer
useEffect does not need to execute, you are passing a function to interceptor, that will executeIquique
@Iquique axios.interceptor was not detecting the API request made in the child components.Incomer
H
33

Create an axios instance using axios.create(config). Use this instance inside useEffect() to add interceptors that can effect the state (reducer is an overkill here). Now use the instance everywhere, and the interceptors will cause a change in the state.

Note: Since multiple requests can start/and or end, you should use a counter. Increment on request, and decrement on response. If the counter is not 0, the application is loading.

const { useState, useMemo, useEffect } = React;

const ax = axios.create(); // export this and use it in all your components

const useAxiosLoader = () => {
  const [counter, setCounter] = useState(0);
  
  useEffect(() => {
    const inc = mod => setCounter(c => c + mod);
    
    const handleRequest = config => (inc(1), config);
    const handleResponse = response => (inc(-1), response);
    const handleError = error => (inc(-1), Promise.reject(error));
  
    // add request interceptors
    const reqInterceptor = ax.interceptors.request.use(handleRequest, handleError);
    // add response interceptors
    const resInterceptor = ax.interceptors.response.use(handleResponse, handleError);
    return () => {
      // remove all intercepts when done
      ax.interceptors.request.eject(reqInterceptor);
      ax.interceptors.response.eject(resInterceptor);
    };
  }, []);
  
  return counter > 0;
};

const GlobalLoader = () => {
  const loading = useAxiosLoader();

  return(
    <div>
    {
      loading ? 'loading' : 'not loading'
    }
    </div>
  );
}

const callApi = (err) => ax.get(err ? 'https://asdf' : 'https://www.boredapi.com/api/activity')

// make a request by using the axios instance
setTimeout(() => {
  callApi();
  callApi(true);
  callApi();
}, 1000);

ReactDOM.render(
  <GlobalLoader />,
  root
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.0/axios.js"></script>
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<div id="root"></div>

Old version:

const { useState, useMemo, useEffect } = React;

const ax = axios.create(); // export this and use it in all your components

const useAxiosLoader = () => {
  const [counter, setCounter] = useState(0);
    
  const interceptors = useMemo(() => {
    const inc = () => setCounter(counter => counter + 1);
    const dec = () => setCounter(counter => counter - 1);
    
    return ({
      request: config => (inc(), config),
      response: response => (dec(), response),
      error: error => (dec(), Promise.reject(error)),
    });
  }, []); // create the interceptors
  
  useEffect(() => {
    // add request interceptors
    const reqInterceptor = ax.interceptors.request.use(interceptors.request, interceptors.error);
    // add response interceptors
    const resInterceptor = ax.interceptors.response.use(interceptors.response, interceptors.error);
    return () => {
      // remove all intercepts when done
      ax.interceptors.request.eject(reqInterceptor);
      ax.interceptors.response.eject(resInterceptor);
    };
  }, [interceptors]);
  
  return [counter > 0];
};

const GlobalLoader = () => {
    const [loading] = useAxiosLoader();
    
    return(
      <div>
      {
        loading ? 'loading' : 'not loading'
      }
      </div>
    );
}

const callApi = (err) => ax.get(err ? 'https://asdf' : 'https://www.boredapi.com/api/activity')

// make a request by using the axios instance
setTimeout(() => {
  callApi();
  callApi(true);
  callApi();
}, 1000);

ReactDOM.render(
  <GlobalLoader />,
  root
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.19.0/axios.js"></script>
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<div id="root"></div>
Hamby answered 14/12, 2019 at 18:28 Comment(16)
thanks, @Ori Drori but I couldn't test it as this code is running into an infinite loop when I place it into the application. Could you please provide the updated one.So that i can accept this as answer.Incomer
Add a console.log() to the counter (inside the hook), and see if to goes from 0 to a number of requests, and then back to 0. If not, you've got some request that is called multiple times in the background (polling for example), and the counter never resets.Hamby
Here is the demo on codesandbox.io/s/serene-kapitsa-x4qbnIncomer
The setTimeout block is not part of the code. Make your calls in other components, not were the loader is. The global loader should not make requests, just show and hide the loader. Create another component, that is not under GlobalLoader, and make the requests from the component. For this example, I've just made several calls as a demo, and I've even made them outside of React.Hamby
This is a better form of example: ` useEffect(() => { // make a request by using the axios instance setTimeout(() => { ax.get("swapi.co/api/people/1"); ax.get("swapi.co/api/people/2"); ax.get("swapi.co/api/people/3"); }, 1000); }, []);`. Think about it a global setting, which is not tied to the call.Hamby
And here is an example of usage: codesandbox.io/s/fervent-jackson-3y4b8Hamby
Thanks you very much, it's work well. But when I add a data (POST request for add data in a state) I have this error Warning: 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. I checked well and the error is indeed caused by the loader code. Do you have a lead? thank you in advanceBury
It seems that the interceptor is called when the component is unmounted. It shouldn't be related to the type of the request.Hamby
Thank you very much, your directions inspired me and I solved the problem :)Bury
Are there scenarios where either the request or response goes missing which breaks the counter?Abstemious
Not as far as I checked. It should handle both success and error responses. You can add a console.log(counter) on the custom hook, and see the current state.Hamby
thanks man ! worked awesomely for me. only one concern... it worked without the create just importing axios on the top and using it directly.Portugal
You can use axios directly. I usually create an instance, and configure it (pass an object with options to the .create function.Hamby
can't we also define interceptors inside the useEffect callback, pass empty dependency array and avoid using useMemo at all.?Scrutator
@SumitWadhwa - good call. Updated to do everything in useEffect.Hamby
@OriDrori This is interesting code but I have one remark seems it relies on order of component rendering, which docs mention you shouldn't rely on: "In general, you should not expect your components to be rendered in any particular order".Treaty

© 2022 - 2024 — McMap. All rights reserved.