useFetch
The useFetch
custom hook is a reusable and simplified way of fetching data from an API endpoint in a React application. It encapsulates the logic required to fetch data, manage the state of the data fetching operation, and provides an easy-to-use interface to the consumers of the hook.
Options
-
url
: The endpoint to fetch data from. -
options
: (optional) An object that allows for configuring the request such as headers, request method, and other parameters.
Returns
-
data
: The data that was fetched from the API endpoint. -
loading
: A boolean flag indicating whether the data is currently being fetched or not. -
error
: An error object that may contain more information about what went wrong with the API call. -
refetch
: A function that can be called to trigger a refetch of the data from the API endpoint. -
isSuccess
: A boolean flag indicating whether the data was successfully fetched and there was no error. -
isError
: A boolean flag indicating whether there was an error while fetching the data.
The Hook
import { useReducer, useEffect, useCallback } from "react";
interface QueryResult<T> {
data: T | unknown;
loading: boolean;
error: Error | null;
refetch: () => void;
isSuccess: boolean;
isError: boolean;
}
interface QueryState<T> {
data: T | unknown;
loading: boolean;
error: Error | null;
}
type QueryAction<T> =
| { type: "FETCH_INIT" }
| { type: "FETCH_SUCCESS"; payload: T }
| { type: "FETCH_FAILURE"; payload: Error };
function queryReducer<T>(
state: QueryState<T>,
action: QueryAction<T>
): QueryState<T> {
switch (action.type) {
case "FETCH_INIT":
return {
...state,
loading: true,
error: null,
};
case "FETCH_SUCCESS":
return {
...state,
loading: false,
data: action.payload,
};
case "FETCH_FAILURE":
return {
...state,
loading: false,
error: action.payload,
};
default:
throw new Error();
}
}
const cache: Record<string, any> = {};
export function useFetch<T>(
url: string,
options?: RequestInit
): QueryResult<T> {
const [state, dispatch] = useReducer(queryReducer, {
data: null,
loading: true,
error: null,
});
const fetchData = useCallback(async () => {
dispatch({ type: "FETCH_INIT" });
try {
const cachedData = cache[url];
if (cachedData) {
dispatch({ type: "FETCH_SUCCESS", payload: cachedData });
} else {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(response.statusText);
}
const jsonData = (await response.json()) as T;
cache[url] = jsonData;
dispatch({ type: "FETCH_SUCCESS", payload: jsonData });
}
} catch (err) {
dispatch({ type: "FETCH_FAILURE", payload: err as Error });
}
}, [url, options]);
useEffect(() => {
fetchData();
}, [fetchData]);
const refetch = useCallback(() => {
delete cache[url];
fetchData();
}, [url, fetchData]);
const isSuccess = !state.loading && !state.error;
const isError = !!state.error;
return {
data: state.data,
loading: state.loading,
error: state.error,
refetch,
isSuccess,
isError,
};
}
Usage
import { useFetch } from "./useFetch";
export default function UseFetchDemo() {
const { data, loading, isSuccess, refetch, isError, error } = useFetch(
"https://jsonplaceholder.typicode.com/photos"
);
return (
<div>
{isError ? (
<div>{error?.message || "something went wrong"}</div>
) : (
<div>
{loading ? (
"Loading..."
) : (
<>
{isSuccess &&
data?.slice(0, 10)?.map((img) => (
<div key={img?.id}>
<img src={img?.url} alt="" />
</div>
))}
</>
)}
</div>
)}
{isSuccess ? (
<button type="button" onClick={refetch}>
Refetch
</button>
) : null}
</div>
);
}