In this article, we will walk you on how to create a custom React hook for remote data fetching with caching.
We will use TypeScript and React’s built-in useReducer
hook to manage the state of our data fetching operations.
Let’s dive into the code for our useApi hook:
import { useReducer } from "react";
import { HTTP_CLIENT } from "../../const";
import { FullUrl, ValueOf } from "@/types";
import { API } from "@/config";
type ApiState<T> = {
status: ValueOf<typeof HTTP_CLIENT>;
loading?: boolean;
error?: unknown;
data: T;
};
type ApiAction<T> =
| { type: typeof HTTP_CLIENT.FETCHING }
| { type: typeof HTTP_CLIENT.FETCHED; payload: T }
| { type: typeof HTTP_CLIENT.ERROR; payload: unknown };
type UseApi<T> = [
(x: (a?: string) => void) => (e: string) => void,
ApiState<T>
];
export function useApi<T>(api: ValueOf<typeof API>): UseApi<T> {
const cache = new Map<FullUrl, T>();
const initialState: ApiState<T> = {
status: HTTP_CLIENT.IDLE,
data: {} as T,
};
const [state, dispatch] = useReducer(
(state: ApiState<T>, action: ApiAction<T>): ApiState<T> => {
switch (action.type) {
case HTTP_CLIENT.FETCHING:
return { ...initialState, status: action.type };
case HTTP_CLIENT.FETCHED:
return { ...initialState, status: action.type, data: action.payload };
case HTTP_CLIENT.ERROR:
return {
...initialState,
status: action.type,
error: action.payload,
};
default:
return state;
}
},
initialState
);
const lazyFetch =
(onSuccess: (e: string) => void) =>
async (q: string): Promise<void> => {
const fullUrl = (api + q) as FullUrl;
dispatch({ type: HTTP_CLIENT.FETCHING });
if (cache.has(fullUrl)) {
const data = cache.get(fullUrl) as T;
dispatch({ type: HTTP_CLIENT.FETCHED, payload: data });
} else {
try {
const res = await fetch(fullUrl);
const data = await res.json();
cache.set(fullUrl, data);
dispatch({ type: HTTP_CLIENT.FETCHED, payload: data });
} catch (error) {
dispatch({ type: HTTP_CLIENT.ERROR, payload: error });
}
}
onSuccess(q);
};
return [
lazyFetch,
{ ...state, loading: state.status === HTTP_CLIENT.FETCHING },
];
}
The ApiState
type, is a comprehensive representation of our data fetching operation’s state, encompassing status, loading indicator, error handling, and the coveted data payload.
The useApi hook is designed to be a reusable and versatile tool for handling API requests and their state. Let’s break down the key components of this hook.
ApiState and ApiAction
ApiState represents the state of the API request and response. It includes properties like status, loading, error, and data. The ApiAction type defines the actions that can be dispatched to modify the state.
UseApi Type
The UseApi type is a tuple that contains two elements:
-1) A function to trigger the API request, which takes a success callback and a query parameter as arguments. -2) An object representing the current state of the API request. Initial Setup Inside the useApi function, we initialize the hook by setting up some initial values. We create a cache to store previously fetched data to minimize unnecessary API requests. We also set an initial state with status, loading, and data properties.
useReducer for State Management
We use useReducer to manage the state of the API request. The reducer function handles different actions to transition between the various states, such as “FETCHING,” “FETCHED,” and “ERROR.” The initial state is set up with the values defined earlier.
The lazyFetch Function
The lazyFetch function is the core of the hook. It is a higher-order function that takes a success callback and returns an asynchronous function to fetch data from the API. It first checks the cache for existing data and, if found, updates the state accordingly. If the data is not in the cache, it sends an HTTP request to the API. Any errors during the process are caught and result in an “ERROR” state.
Return Value
The useApi hook returns a tuple containing the lazyFetch function and the current state of the API request. The state includes status, loading, error, and data, with loading being a boolean representing whether the request is in progress.
How does the implementation looks like ?
To utilize the useApi hook effectively, you can create custom hooks for specific use cases, such as autocomplete functionality.
import { useState, useEffect, useCallback } from "react";
import { ValueOf } from "@/types";
import { useDebounce } from "@/hooks";
import { API } from "config";
import { useApi } from ".";
type UseAutoComplete<ApiResponse> = {
readonly prevQuery?: string;
readonly query?: string;
readonly data: ApiResponse;
readonly error: unknown;
readonly loading?: boolean;
setQuery: React.Dispatch<React.SetStateAction<string | undefined>>;
};
export const useAutoComplete = function <ApiResponse>(
apiUrl: ValueOf<typeof API>
): UseAutoComplete<ApiResponse> {
const [query, setQuery] = useState<string>();
const [prevQuery, setPrevQuery] = useState<string>();
const [lazyFetch, { data, error, loading }] = useApi<ApiResponse>(apiUrl);
const handleFullTextSearch = useCallback(
useDebounce(lazyFetch(setPrevQuery)),
[]
);
useEffect(() => {
if (query) handleFullTextSearch(query);
else setPrevQuery(undefined);
}, [handleFullTextSearch, query]);
return { prevQuery, query, data, error, loading, setQuery };
};