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,
}
}