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)
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;
};