React 18 - useData()
hook with axios and TypeScript
3 min read
Because of the changes from the latest version of React, the useEffect()
runs twice. I created a custom hook which will fix race condition and will fetch and set the data only once using axios.
Implementation:
import axios, { AxiosError, Method } from 'axios';
import { useEffect, useState } from 'react';
// replace this to match your backend's error response body
interface IErrorResponse {
error: { message?: string };
}
// http://localhost:4000 for example
axios.defaults.baseURL = process.env.API_URL;
const useData = <T>(
url: string,
method: Method,
body?: any,
): [boolean, string | null, T | null] => {
const [loading, setLoading] = useState<boolean>(false);
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
// from https://beta.reactjs.org/learn/you-might-not-need-an-effect#fetching-data
let ignore = false;
/**
* used to abort an ongoing request, it will not work with Internet Explorer
* it can be removed and use just the `ignore` variable
*/
const controller = new AbortController();
const fetchData = async () => {
try {
setLoading(true);
const response = await axios({
url: url,
method: method,
data: body,
cancelToken: source.token,
});
const data = response?.data;
if (!ignore) {
setData(data);
}
} catch (err: any) {
const error = err as Error | AxiosError<IErrorResponse>;
// replace here with your own error handling
if (axios.isAxiosError(error)) {
if (error.code === AxiosError.ERR_CANCELED) {
console.info('Request was canceled');
return;
}
if (!ignore) {
setError(error?.response?.data?.error?.message || error.message);
}
} else {
if (!ignore) {
setError(error.message);
}
}
console.error(err);
} finally {
if (!ignore) {
setLoading(false);
}
}
};
fetchData().then((r) => r);
// To fix the race condition, we need to add a cleanup function to ignore stale responses:
return () => {
ignore = true;
controller.abort();
};
}, [url]);
return [loading, error, data];
};
export default useData;
Usage:
import { Link } from 'react-router-dom';
import useData from '@/hooks/use-data';
interface IMovie {
id: number;
title?: string;
description?: string;
year?: number;
releaseDate?: string;
}
interface IAllMoviesResponse {
movies: IMovie[];
}
export default function Movies() {
const [loading, error, data] = useData<IAllMoviesResponse>(`/v1/movies`, 'GET');
if (loading) {
return <div>Loading..</div>;
}
if (error) {
return (
<div>
Error: <code>{error}</code>
</div>
);
}
return (
<>
<h2>Choose a movie</h2>
<ul>
{data?.movies.map((m) => (
<li key={m.id}>
<Link to={`/movies/${m.id}`}>{m.title}</Link>
</li>
))}
</ul>
</>
);
}
These are some snippets from one of my pet projects where I study GO: Full code.
- #react_18
- #axios
- #typescript
- #javaScript
- #custom_hook