Search code examples
javascriptreactjspromisefetch

infinite fetch request with react-promise-tracker


I am fetching data from an external API using the fetch API. I would like to implement a loading cursor while the request is fetching the data. For this I'm trying to use the react-promise-tracker library. But when using the trackPromise from the library, the fetching repeats itself infinitely.

You can see the bug happening yourself when changing the commented code in this example project (check the console this is all happening in console.log): https://codesandbox.io/s/tender-easley-szfxg?file=/src/App.tsx

Basically this works:

export default function App() {
  const get = (url: string) => {
    let headers: {
      Accept: string;
      "Content-Type": string;
    } = {
      Accept: "application/ld+json",
      "Content-Type": "application/json"
    };
   
    return fetch(url, {
      method: "GET",
      headers
    } as RequestInit)
      .then((res: Response) => {
        if (res.ok) {
          return res.json();
        }

        return Promise.reject(res);
      })
      .catch(Promise.reject.bind(Promise));
  };
  get("https://608bb5b6737e470017b752e2.mockapi.io/users")
    .then(console.log)
    .catch(console.log);

  return <div>nothing</div>;
}

this doesn't:

import { usePromiseTracker, trackPromise } from "react-promise-tracker";   // ADDED CODE

export default function App() {
  const { promiseInProgress } = usePromiseTracker({ area: "fetchDataGet" });      // ADDED CODE
  const cursorStyle = () => {                                                     // ADDED CODE
    return promiseInProgress ? { cursor: "wait" } : undefined;                    // ADDED CODE
  };                                                                              // ADDED CODE
  const get = (url: string) => {
    let headers: {
      Accept: string;
      "Content-Type": string;
    } = {
      Accept: "application/ld+json",
      "Content-Type": "application/json"
    };
    return trackPromise(                                                           // ADDED CODE
      fetch(url, {
        method: "GET",
        headers
      } as RequestInit)
        .then((res: Response) => {
          if (res.ok) {
            return res.json();
          }

          return Promise.reject(res);
        })
        .catch(Promise.reject.bind(Promise)),
      "fetchDataGet"                                                               // ADDED CODE
    );
  };
  get("https://608bb5b6737e470017b752e2.mockapi.io/users")
    .then(console.log)
    .catch(console.log);

  return <div style={cursorStyle()}>nothing</div>;
}

The ouput of the second code is several console.log when it should be only 1 and at some point the servers returns a 429 error (which is normal, it's just server security)


Solution

  • In your second example you are not using useEffect and instead call get() inside of the component body (which means it will be called every time the component renders). usePromiseTracker will trigger a state change and therefore a re-render every time your request promise resolves. This will definitely lead to an infinite loop of fetching.

    Generally speaking you should never ever unconditionally trigger any side-effects (like fetching data) inside of the component body. It should always be inside of either an effect that only runs at certain points in time or inside of an event handler.

    In this specific case you could solve it by warping you get() call in an effect:

    useEffect(() => {
        get("https://608bb5b6737e470017b752e2.mockapi.io/users")
        .then(console.log)
        .catch(console.log);
    }, []); // will only be called once after mounting
    

    If you have this kind of code frequently in your project you should extract a custom hook that does this internally and also hides usePromiseTracker so you don't have to explicitly use it every time you want to fetch something:

    const useFetch = (area, url, options) => {
        const { promiseInProgress } = usePromiseTracker({area});
    
        useEffect(() => {
            trackPromise(                                                           
                fetch(url, options)
                    .then(res => res.ok ? res.json() : Promise.reject(res))
                    .catch(Promise.reject.bind(Promise)),
                area,                                                               
            );
        }, []);
    
        return promiseInProgress;
    };