I created a react hook to perform api calls inspired by this article.
The hook looks as followed:
function useApi<T>(apiFunc: (...args: Array<any>) => Promise<AxiosResponse<T>>) {
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const controllerRef = useRef(new AbortController());
const request = async (...args: Array<any>) => {
setLoading(true);
try {
const response = await apiFunc(...args);
setData(response.data);
} catch (error: any) {
setError(error.message || 'Unknown Error');
} finally {
setLoading(false);
}
};
return { request, cancel, data, error, loading };
};
Now I am using the hook in a component in the useEffect function:
const vacancyApi = useApi(getVacancy);
useEffect(() => {
if (!!id) {
vacancyApi.request(id);
}
}, [id, vacancyApi]);
The problem is that when I add vacancyApi
to the dependency array it results in an endless loop because vacancyApi is updated by the hook which then calls useEffect again.
I could just not add vacancyApi
to the dependency array and it works but that seems ugly to me and not the right thing to do.
I also can’t move the function out of the component because it’s a hook and has to be created inside the component.
In the articles comments I read that it could be a solution to wrap the request function into a useCallback hook. But I haven’t figured out how to do it properly, I still had the same problem.
const request = useCallback(async (...args: Array<any>) => {
...
}, [apiFunc]);
Is there a solution where I can call the api only if the id changes without removing the dependency of vacancyApi
from the useEffect dependencies?
2
Answers
As you have it now, your
useApi
hook is going to create a newrequest
function every time it’s called, so you need to put that in auseCallback
:In order this to work, this also requires that
apiFunc
is a constant value from one render to the next.Once you’ve ensured that
request
isn’t constantly changing, you need to use that in the dependency array for youruseEffect
not the whole object thatuseApi
returns:I believe that will do the trick.
no more
any
@JLRishe helps you solve your infinite loop but there’s other issues with your code that are less straightforward. The biggest one being
(...args: Array<any>) => ...
effectively disabling TypeScript wherever this hook is used. Let’s fix that using a more generic hook –Why that
mounted
var? We need to make sure we don’t attempt to set state on an unmounted component. See the React guide for Fetching Data.Now separate axios logic from your components with a reusable
api
module –Type-safety restored. Now your components simply glue the pieces together without involving complex logic –
no more nulls
The
UseAsyncHookState
type has nullable fields which means we have to be mindful in the way we attempt to access the result. Always checkingloading
first, then for the presence oferror
before accessingdata
. We can take advantage of TypeScript 5’s new exhaustive switch..case completions by adjusting our hook’s type –Now we can write these in any order without worry and TypeScript will warn us if we forget to handle all
kind
s –multiple uses of useAsync
You can see a more complex use case where multiple
useAsync
calls are needed in this Q&A.