React

Fetching

Description:

Implementation of a custom hook to fetch data in react. The idea here is that we are going to iterate the implementation and make it more complete on each step. The final implementation and how can be used is at the end.

Result of the fetching:

Fetched: undefined posts.

1. Basic implementation

The basic implementation of the fetch custom hook is going to get a URL as parameter and it's going to return the data that we got. In this case, we are hardcoding the GET method but we are going to change that later.

The way React works, it doesn't allow us to use async method inside components so what we are going to do is to call the async function inside the useEffect() hook. Here we are going to call the base fetch function and we are going to await it's response and set the data.

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 interface PropType { url: string; }; const useFecth = ({url}: PropType) => { const [data, setData] = useState(null); useEffect(() => { const fetchDataAsync = async () => { const response = await fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/json' } }); const data = await response.json(); setData(data); } fetchDataAsync(); }, [url]); return { data }; };

2. Adding loading state

We are going to add the isLoading state on the custom hook so we can return if the data is being fetched.

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 const useFecth = ({url}: PropType) => { const [data, setData] = useState(null); const [isLoading, setIsLoading] = useState<boolean>(false); useEffect(() => { const fetchDataAsync = async () => { const response = await fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/json' } }); const data = await response.json(); setData(data); setIsLoading(false); } setIsLoading(true); fetchDataAsync(); }, [url]); return { data, isLoading }; };

3. Adding abort controller

We are also going to add the Abort Controller . This object allow us to abort the fetch request if needed.

Why would we cancel the request? Well, let's say that the fetch that we are doing is heavy, and it takes 2 seconds. What would happen if the component gets unmounted while whe are waiting for it? We don't want that so what we are going to do is cancel that request before is completed.

In order to do this we are going to pass the controller in the request, and we are going to controller.abort() when the component get's unmounted.

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 const useFecth = ({url}: PropType) => { const [data, setData] = useState(null); const [isLoading, setIsLoading] = useState<boolean>(false); useEffect(() => { const controller = new AbortController(); const fetchDataAsync = async () => { const response = await fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/json' }, signal: controller.signal, }); const data = await response.json(); setData(data); setIsLoading(false); } setIsLoading(true); fetchDataAsync(); return (() => { controller.abort(); }); }, [url]); return { data, isLoading }; };

4. Adding mounted guardrails

But what happens if the fetch is completed and the component is unmounted right after it? We still need to process the response and we are still using the state for the data, and if the component gets unmounted this is going to fail.

So what we are doing is using a simple boolean to check if the component is mounted or not, if it is we are going to continue using the parts of the hook that needs the component to be mounted

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 const useFecth = ({url}: PropType) => { const [data, setData] = useState(null); const [isLoading, setIsLoading] = useState<boolean>(false); useEffect(() => { let isMounted = true; const controller = new AbortController(); const fetchDataAsync = async () => { const response = await fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/json' }, signal: controller.signal, }); const data = await response.json(); if(isMounted) { setData(data); setIsLoading(false); } } setIsLoading(true); fetchDataAsync(); return (() => { controller.abort(); isMounted = false; }); }, [url]); return { data, isLoading }; };

5. Handeling errors

Now we need to take care of the errors. Any call to a another service can fail, and we need to catch those errors and handle them so the app doesn't stop working.

In this case, we are wrapping the functionality in a try / catch statement so we can catch any errors that ocurs inside of it and we are going to be saving that error an returning it so the component that called the hook can do something with it (like showing the error in the UI or retrying).

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 const useFecth = ({url}: PropType) => { const [data, setData] = useState(null); const [isLoading, setIsLoading] = useState<boolean>(false); const [error, setError] = useState<Error | undefined>(); useEffect(() => { let isMounted = true; const controller = new AbortController(); const fetchDataAsync = async () => { try { const response = await fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/json' }, signal: controller.signal, }); const data = await response.json(); if(isMounted) setData(data); } catch (error) { if(isMounted) setError(error as Error); } finally { if(isMounted) setIsLoading(false); } } setIsLoading(true); fetchDataAsync(); return (() => { controller.abort(); isMounted = false; }); }, [url]); return { data, isLoading, error }; };

6. Adding a return type with generics

Another thing that we want to do is to make it type safe. In order to do this, we are going to receive a generic type that we are going to use to cast the data that we are fetching

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 interface ReturnType<T> { data: T | undefined; isLoading: boolean; error: Error | undefined; } const useFecth = <T>({url}: PropType): ReturnType<T> => { const [data, setData] = useState<T | undefined>(); const [isLoading, setIsLoading] = useState<boolean>(false); const [error, setError] = useState<Error | undefined>(); useEffect(() => { let isMounted = true; const controller = new AbortController(); const fetchDataAsync = async () => { try { const response = await fetch(url, { method: 'GET', headers: { 'Content-Type': 'application/json' }, signal: controller.signal, }); const data = await response.json(); if(isMounted) setData(data as T); } catch (error) { if(isMounted) setError(error as Error); } finally { if(isMounted) setIsLoading(false); } } setIsLoading(true); fetchDataAsync(); return (() => { controller.abort(); isMounted = false; }); }, [url]); return { data, isLoading, error }; };

7. Final implementation

Last but not least we are going to refactor the hook so we can accept other tpyes of request such as GET / POST / DELETE etc. In order to do this, we are receiving a new optional prop called init that is going to be a RequestInit object with all the optional values that you can pass to the request.

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 import { useEffect, useState } from "react"; interface PropType { method: 'GET' | 'PUT' | 'POST' | 'PATCH' | 'DELETE' | 'TRACE' | 'HEAD' | 'CONNECT' | 'OPTIONS'; url: string; init?: RequestInit; }; interface ReturnType<T> { data: T | undefined; isLoading: boolean; error: Error | undefined; } const useFecth = <T>({method, url, init}: PropType): ReturnType<T> => { const [data, setData] = useState<T | undefined>(); const [isLoading, setIsLoading] = useState<boolean>(false); const [error, setError] = useState<Error | undefined>(); useEffect(() => { let isMounted = true; const controller = new AbortController(); const fetchDataAsync = async () => { try { const response = await fetch(url, { ...init, method: method, signal: controller.signal, }); const data = await response.json(); if(isMounted) setData(data as T); } catch (error) { if(isMounted) setError(error as Error); } finally { if(isMounted) setIsLoading(false); } } setIsLoading(true); fetchDataAsync(); return (() => { controller.abort(); isMounted = false; }); }, [url, method, init]); return { data, isLoading, error }; }; export default useFecth;

8. Usage

Finally, if we want to test this hook, what we can do is to create a custom type

0 1 2 3 4 5 6 interface TestType { body: string; id: number, title: string; userid: number; }

And we can fetch the endpoint using the custom type.

0 1 2 3 4 const {data, isLoading, error} = useFecth<TestType[]>({ method: 'GET', url: 'https://jsonplaceholder.typicode.com/posts' });
๐Ÿ›๏ธ ๐Ÿงฎ ๐Ÿ“ โš›๏ธ ๐Ÿงช ๐Ÿ