🪝 Building custom "useTypedFetch" hook in React with TypeScript
Tags: react, typescript, beginners, webdev
Published: 2021-09-29 11:24:29

Asynchronous interactions are the bread-and-butter of modern JavaScript programming. Let’s see how we can abstract away all the heavy lifting and boilerplate of data fetching in a custom **useTypedFetch** hook 🪝.

✨ This article is particularly aimed at beginners and those who want to familiarize themselves with async TypeScript a little better and maybe start using it with React. 

###Motivation###

Since it might be tricky to work with dynamic types of data in TypeScript (and almost all api calls, per se, are purely dynamic), we want to build a flexible tool that will adapt to any kind of an api response and will do this dynamically. 

Additionally, it would be great to make this hook **"http-client-agnostic"**. Put simply, this hook should get a standardized input (url, method, headers etc) and should seamlessly work with different types of http-clients (fetch, axios etc). This will allow us to easily migrate from one http-client solution to another(if need be) without rewriting hundreds of api calls and thousands of line of code. 

**❗Note** : This is for learning and academic purposes only. In production, I would advise to rely on established solutions and libraries.

###Foundation###

Let’s start from the brain of our fetching logic - fetch client or request function. It should be able to make all types of calls (‘GET’, ‘PUT’, ‘POST’, ‘DELETE’, etc). It’s **single responsibility** should be just making calls using native fetch or axios (or some other library). By delegating all calls in our app to this one fetch client, we can make our app far more robust, since calls will not be diluted all over the project(and when we will decide to migrate to some other fetching library or solution this will be super easy).

Let’s start from the interface of our fetch-client function.
If we use native fetch, we can automatically make a “GET” request by only specifying “url”. Other params are optional. So let’s mimic standard fetch interface


```typescript
interface RequestConfig {
 url: string; 
 method?: string;
 headers?: Record<string, string>; //💡 “?” - specifies optional field
 data?: Record<string, string | number>;    
}
```
💡 Notice, headers and data use nifty Record utility which actually constructs an object type, whose property keys are of first type specified in generic( in our example - string) and values are specified by second type in generic.

For simplicity’s sake, we will not include a list of all possible params. 
❗ In “headers” and “data” we will be specifying config objects to be added to headers and body 

For instance,
```typescript
headers: {
     'Content-Type': 'application/json',
   }
```
###Global Fetch Function###

Now we are ready to build our global request function, it takes an object with interface ***RequestConfig*** and returns a promise as all async functions do: 

```typescript
export const makeFetchRequest = ({
 url = '/',
 method = 'get',
 data,
 headers,
}: RequestConfig): Promise<Response> => {
 return fetch(url, { method, headers, body: JSON.stringify(data) });
};

```
💡 Notice that the input params have RequestConfig type which we will soon use for another variant using “axios”. This current implementation is based on ***native “fetch”***. Additionally we specified in the generic of returned promise - `<Response>` which is a native fetch response (IDE will provide useful hints).

Here how it looks under the hood... 
```typescript
/** This Fetch API interface represents the response to a request. */
interface Response extends Body {
   readonly headers: Headers;
   readonly ok: boolean;
   readonly redirected: boolean;
   readonly status: number;
   readonly statusText: string;
   readonly type: ResponseType;
   readonly url: string;
   clone(): Response;
}
```
### We’re half way through! 🍾###

As you might have noticed, all our accomplishments have nothing to do with React (which is great). Because we could abstract away our fetch logic even from the framework and later re-use or consume it in other frameworks ( if we use micro-frontend architecture with many frameworks).

***Let’s now get back to the React land and think about the basic state of our useTypedFetch hook.***

In its simplest implementation, it should receive a url and request options and hopefully return some data, error and loading indicators after making some api call.

Since we already created an interface for our makeFetchRequest function, let’s re-use it!

**Here is our useFetch Function initial signature**

```typescript
export const useTypedFetch = ({ url }: RequestConfig) => {
// ⭐ api response data will be stored here!
 const [fetchedData, setFetchedData] = useState<any>(null);
// ⭐ loading flag
 const [isLoading, setIsLoading] = useState<boolean>(false); 
// ⭐ errors piece of state
 const [error, setError] = useState<any>(null);
 
 /*
   🌠 Some magic happens here 🌠
 */
 
 return { fetchedData, isLoading, error };
};

```

💡 **The biggest problem** with async operations is that we don’t know what the type of an api response we will get beforehand. So we cannot hard-code it here in useTypedFetch. 
❗ We also want to make this hook *adaptable* to any kind of API (and not resort to **any** type or cumbersome type narrowing with **unknown**)

Sounds really complicated to create a function that will use a type that we don’t know beforehand, but **the solution** is really simple - **generics** 💡.

###Let’s start from the type of what we will actually get from useTypedFetch ###

We will call this type very simply - UseTypedFetchReturn

```typescript
type UseTypedFetchReturn<T> = {
 data: T | null;
 isLoading: boolean;
 error: string | null;
};
```
Generic `<T>` will be added at `useTypedFetch` function’s call-time and we will be able to specify it and receive all the type safety and hinting support of TypeScript. Which makes me happy! 🤗

**Let’s implement this in the hook**
```typescript
// ❗Notice we put `<T>` before function's param list
export const useTypedFetch = <T>({
 url,
 headers,
 method,
 data,
}: RequestConfig): UseFetcherReturn<T> => {
 //⭐ We also use T in generic of fetchedData, since it essentially what we will get from an API
 const [fetchedData, setFetchedData] = useState<T | null>(null);
 const [isLoading, setIsLoading] = useState<boolean>(false);
 const [error, setError] = useState<string | null>(null);
 
 /*
   🌠 Some magic will soon happen here, be patient 🌠
 */
 
 return { data: fetchedData, isLoading, error };
};
```

💡 **Generic `<T>`** that is added before the function will specify our API response data type (which we can get from our backend libs or specify on our own). We can use any name instead of `<T>`, for instance `<SomeCoolGenericType>`. 

💡 Notice that at this stage we firstly specify this generic type before the hook and then “consume” it in the hook’s return type `UseFetcherReturn<T>` and in local data state `useState<T | null>(null)`. Essentially, we construct our whole “type-flow” based on this specified type. Sweet! 🤗

✨ Voila, now we can specify any type before each fetch call in useTypedFetch and get all the Typescript **hints and benefits** dynamically.✨


###Now we are ready to actually fetch###

Let’s add useEffect and update our hook’s pseudocode

```typescript
export const useTypedFetch = <T,>({
 url,
 headers,
 method,
 data,
}: RequestConfig): UseTypedFetchReturn<T> => {
 const [fetchedData, setFetchedData] = useState<T | null>(null);
 const [isLoading, setIsLoading] = useState<boolean>(false);
 const [error, setError] = useState<string | null>(null);
 return { data: fetchedData, isLoading, error };
};
 useEffect(() => {
   try {
     setLoading(true);
     const data = makeSomeRequest();
     setFetchedData(data);
     setIsLoading(false);
   } catch (error) {
     setIsLoading(false);
     doSomethingWithError();
     setError(error);
   }
 }, []);
```
We probably will **makeSomeRequest()**, and if everything will be OK, we will store api response data in the local state and return it ready for further consumption from the hook. Otherwise we will **doSomethingWithError()** (be it a second call, abort, log error or simply store error in the local state and return it).

💡 However, I really want to abstract the logic of makeSomeRequest() away from the hook (like we did with makeFetchRequest ) and create a helper function. Let’s name it **“fetcher”**.

In this helper we will use our main fetch-client “makeFetchRequest”, and it will look like:

```typescript
export const fetcher = async <T,>({
 url,
 method,
 headers,
 data,
}: RequestConfig): Promise<T> => {
 // ⭐ make api call
 const apiResponse = await makeFetchRequest({ url, method, headers, data });
 // ⭐ call json() to transform a Response stream into usable JSON
 const apiData: T  = await apiResponse.json();
 return apiData;
};
```
💡 Notice we again re-use the RequestConfig interface and use generic  <T> to specify what will be returned. **Fetcher** is an async function, so it obviously returns a promise.


Let’s get back to the hook and integrate this fetcher helper.

```typescript
export const useTypedFetch = <T,>({
 url,
 headers,
 method,
 data,
}: RequestConfig): UseTypedFetchReturn<T> => {
 const [fetchedData, setFetchedData] = useState<T | null>(null);
 const [isLoading, setIsLoading] = useState<boolean>(false);
 const [error, setError] = useState<string | null>(null);
 
 useEffect(() => {
   //⭐ we use IIFE to automatically invoke fetcher
   (async () => {
     try {
       setIsLoading(true);
       const res = await fetcher<T>({ url });
       setFetchedData(res);
       setIsLoading(false);
     } catch (err) {
       setIsLoading(false);
       //⭐ here we can narrow the type of error (if for instance we have our own custom error class or do something else)
       if (err instanceof Error) {
         setError(err.message);
       }
     }
   })();
 }, [url]);
 
 return { data: fetchedData, isLoading, error };
};
```
✨ Now we can use this hook with any kind of an API and get all the type safety and convenient hinting along the way. ✨

###Using the hook###
Let’s get back to React land and make several api calls. We will test our hook with GET and POST requests. We will use https://jsonplaceholder.typicode.com/ as our mock backend API.

You could play around with the working example on CodePen
{% codepen https://codepen.io/vovaushenko/pen/bGRQbRE %}


In a nutshell, we specify types of Todo and Post which will be returned from API stored in arrays.

1️⃣ We make GET calls to API/todos and API/posts to get data. 

2️⃣ We also make a POST call to store a new post. 

3️⃣ We also use this data to render basic JSX.

```typescript
/**
* INTERFACES OF https://jsonplaceholder.typicode.com/
*/
interface Todo {
 userId: number;
 id: number;
 title: string;
 completed: boolean;
}
interface Post {
 userId: number;
 id: number;
 title: string;
 body: string;
}
 
const API = 'https://jsonplaceholder.typicode.com';
 
function App() {
 //⭐ Example of making GET call to get array of Todos
 const {
   data: todos,
   error,
   isLoading,
 } = useTypedFetch<Todo[]>({
   url: `${API}/todos`,
 });
 //⭐ Example of making GET call to get array of Posts
 const { data: posts } = useTypedFetch<Post[]>({
   url: `${API}/posts`,
 });
 //⭐ Example of making POST request to create a new post, no pun intended
 const { data: postData } = useTypedFetch<Post>({
   url: `${API}/posts`,
   method: 'POST',
   data: { title: 'foo', body: 'bar', userId: 1 },
   headers: {
     'Content-type': 'application/json; charset=UTF-8',
   },
 });
 
 return (
   <Container>
     {todos?.slice(0, 3).map((todo) => (
       <article>
         <h2>{todo.title}</h2>
         <p>{todo.completed}</p>
       </article>
     ))}
     {posts?.slice(0, 3).map((post) => (
       <article>
         <h2>{post.title}</h2>
         <p>{post.body}</p>
       </article>
     ))}
 
     <article>
       Here is our newly POST-ed post
       {JSON.stringify(postData, null, 2)}
     </article>
 
     {error && <h1>{error}</h1>}
   </Container>
 );
}
```
I think everything is pretty basic and self-explanatory. Please notice that in the generic of useTypedFetch we specify what kind of data shape we are expecting and right away in the code we will get useful code completion and hinting. Which makes me happy 😍

##Using with axios##
Finally! Let’s enhance our hook with variability. So far, we’ve constructed it on top of fetch. Let’s add axios!

Our initial step was to specify the interface of request configuration and axios already did all the heavy-lifting and provides AxiosRequstConfig interface.

Let’s build our global request function.
```typescript
export const makeAxiosRequest = <T,>({
 url = '/',
 method = 'get',
 data,
 headers,
}: AxiosRequestConfig): AxiosPromise<T> => {
 return axios({ url, method, data, headers });
};
```
💡 As you might have noticed it looks like our initial **makeFetchRequest** function. Of course it has built-in axios interfaces and it uses axios as the http-client. But it takes exactly the same params as input, which is great!

Let’s add a axios fetcher function, like previously with “fetcher”.

```typescript

const axiosFetcher = async <T,>({
 url,
 method,
 headers,
 data,
}: AxiosRequestConfig): Promise<T> => {
 const { data: apiData } = await makeAxiosRequest<T>({
   url,
   method,
   headers,
   data,
 });
 return apiData;
};
```
💡 If you will compare it with our initial fetcher, you will notice that it takes exactly the same input and produces exactly the same output! We now actually have two options in our useTypedFetch, we can use either fetch or axios ✨!

Let’s see this in the hook
```typescript
export const useTypedFetch = <T,>({
 url,
 headers,
 method,
 data,
}: RequestConfig): UseTypedFetchReturn<T> => {
 const [fetchedData, setFetchedData] = useState<T | null>(null);
 const [isLoading, setIsLoading] = useState<boolean>(false);
 const [error, setError] = useState<string | null>(null);
 
 useEffect(() => {
   (async () => {
     try {
       setIsLoading(true);
       // ⭐⭐⭐ Both res1 and res2 return 
       //the same response of the same type! 
       //Now we can have a flexibility to use either
       // fetch or axios ⭐⭐⭐
       const res1 = await axiosFetcher<T>({ url, headers, method, data });
       const res2 = await fetcher<T>({ url, headers, method, data });
       setFetchedData(res1);
       setIsLoading(false);
     } catch (err) {
       setIsLoading(false);
 
       if (err instanceof Error) {
         setError(err.message);
       }
     }
   })();
 }, []);
 
 return { data: fetchedData, isLoading, error };
};
```


🍾 That’s it guys! Hope you enjoyed this simple overview and learned something new!

**Cheers!** 🙌

*You can find the code used in this blogpost at the following sandbox URL:* https://codepen.io/vovaushenko/pen/bGRQbRE