I created a custom React hook to return a function and the returned function takes a config object and makes Axios API calls, the custom hook has three states loading, error and data which is also returned from the custom hook. see this codesandbox for code
So the hook is doing what it's supposed to do I can log the data fine, the view is getting updated with the data but my issue is in this example in app.js where I'm handling api call on a button click data is not available inside that click handler function on the first click but if I click the second time I do see the data (see if statement on line 16 of app.js) ideally instead of console logging I would like to perform some action on the data like dispatch an action to update redux state for example... from my understanding useState inside of my custom hook is async and requires a re-render to get the data so if I were to list data as a dependency in my useEffect hook I can see the data whenever I make the call but I'm a bit confused is there a way to make the data available inside my clickHandler function? to be honest I'm not quite sure if my approach is correct in this case I could be thinking wrong about this whole thing! any insight on this would be appreciated.
I've been on numerous SO, blog posts and youtube videos but most of them are using useEffect hook inside the custom hook and returning data from there which I do not want to do since I want to use the custom hook for api calls on demand whenever I call the function in this case when I click a button.
React doesn't guarantee that the state will be immediately updated after setState()
. This is because state updates are non-blocking which means that code execution within the click handler will not wait for the state update to finish. Even if you try using await
with the API_REQUEST()
, it will still not immediately reflect. State updates will be added to the task queue and the code will continue executing past this point, therefore when it reaches the if statement, the data will not be up to date at this point.
In old class component days, we could pass a callback to setState()
as a second argument which would allow us to do anything with upto date state.
this.setState(counter => counter + 1, updated => console.log(updated));
But with functional components, we can no longer do this and React docs recommend using effect hooks. Therefore, you have to implement a similar behavior yourself if you need it, by adding some kind of callback to your custom hook, that will be run when the API request is made. It could be done using useEffect()
inside your custom hook.
You can make the useEffect()
run only after data has changed by adding only data
as a dependency:
useEffect(() => {
}, [data]);
However, as you said you only want this to run on click and the above doesn't tell how the data actually changed. So we have to think outside the box here i.e. do we really need to take the data from the state? I think we can take the data by passing a callback as a second argument to API_REQUEST()
and bypass state:
import { useEffect, useState } from "react";
const useAxios = () => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [data, setData] = useState(null);
const [controller, setController] = useState();
const API_REQUEST = async (config, callback) => {
const { AxiosInstance, method, url, requestConfig } = config;
try {
setLoading(true);
setError(null);
setData(null);
const ctrl = new AbortController();
setController(ctrl);
const response = await AxiosInstance({
method,
url,
...requestConfig,
signal: ctrl.signal
});
setData(response.data);
callback(response.data);
} catch (error) {
console.error({ error });
setError(error.message);
} finally {
setLoading(false);
}
};
useEffect(() => {
return () => controller && controller.abort();
}, [controller]);
return { loading, error, data, API_REQUEST };
};
export default useAxios;
Now you may use API_REQUEST()
like this:
API_REQUEST(
{
AxiosInstance: axios,
method: "get",
url: "/todos",
requestConfig: {}
},
(responseData) => console.log(responseData)
);
Here is a working example: https://codesandbox.io/s/api-request-callback-q40bxr