I have a project where I created a custom hook "useApi" that manage calls to my backend API to avoid code repetition everytime I need to call the API.
This useApi hook return an array with a loading state as first parameter, and the function to call the API as second parameter.
The loading state is set to true by default, and is set to false when fetch is done.
My problem comes here, in some scenarios I need to call the API on events (clicks and forms submit). In that case I would prefere the loading state to false by default, then set it to true before the fetch, and back to false when fetch is done. In the code of my custom hook below, I commented the lines setLoading(true) before the fetch otherwise it causes an infinite loop.
I think I kind of understand why this infinite loop because of the loading state update. But I can't find a solution on how to achieve this "dynamic" loading state
import { useState } from "react";
export default function useApi({method, url}) {
const [loading, setLoading] = useState(true);
const abortController = new AbortController();
const fetchBaseUrl = import.meta.env.VITE_APP_URL + '/api/' + url;
const methods = {
get : function(data = {}) {
// setLoading(true);
const params = new URLSearchParams(data);
const queryString = params.toString();
const fetchUrl = fetchBaseUrl + (queryString ? "?"+queryString : "");
return new Promise((resolve, reject) => {
fetch(fetchUrl, {
signal : abortController.signal,
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
},
})
.then(response => response.json())
.then(data => {
setLoading(false);
if (!data) {
return reject(data);
}
resolve(data);
})
.catch(error => {
setLoading(false);
if (!abortController.signal.aborted) {
reject(error);
}
});
});
},
post : function(data = {}) {
// setLoading(true);
const bodyData = {
signal : abortController.signal,
...data
};
return new Promise((resolve, reject) => {
fetch(fetchBaseUrl, {
method: "post",
headers: {
"Content-Type": "application/json",
"Accept": "application/json",
},
body: JSON.stringify(bodyData)
})
.then(response => response.json())
.then(data => {
setLoading(false);
if (!data) {
return reject(data);
}
resolve(data);
})
.catch(error => {
setLoading(false);
if (!abortController.signal.aborted) {
reject(error);
}
});
});
}
}
if ( !(method in methods) ) {
throw new Error("Incorrect useApi() first parameter 'method'")
}
return [loading, methods[method]];
}
What I tried :
- Replace loading state by using useRef() hook :
const loading = useRef(false);and update loading.current before and after fetch, but this didn't work, in my component where I called the custom hooke, the loading value received isn't updated - Call fetch functions in a useEffect with a 'data' state dependency (
const [data, setData] = useState({})), and returning setData instead of fetch functions, didn't work either, the setData made an infinite loop on some cases (in a react-rooter loader function I had an infinite loop, but not on en event submit event)