Papercut pit bulls waring space suits flying and playing fetch

React useFetch hook

Intro

Today I feel like sharing some code snippets that I use across my projects. Having a repository or even private npm package stocked with a suite of tools is always a good idea - it can drastically improve your performance.

This post focuses on fully-typed React hook snippet for data fetching. So, let's dive in!

FetchReturnType utility type


Our first task is to create a utility type named FetchReturnType. This type becomes invaluable in our utilities, encapsulating the fetched data type, 'isFetching' state, and potential errors.

1interface FetchReturnType<Data> {
2  data: Data | null;
3  isFetching: boolean;
4  error: string | null;
5}

useFetch

Choosing the right tools is crucial for developers. When working with a straightforward app that only sends a few requests, it's unnecessary to overload it with external dependencies. Our approach is to create a hook that uses the native fetch API and supports cancellation through the AbortController. We'll also focus on handling network and request-related errors.

Querying

Let's begin by establishing the foundation for our hook, concentrating on its primary function: data fetching. Starting with a basic fetch request, we'll need a URL and plan to use the GET HTTP method, incorporating our FetchReturnType utility type.

Here's how to create an asynchronous function that takes a URL as input and returns data:

1async function fetchData<Result>(url: string): Promise<Result> {
2  const response = await fetch(url);
3  const result: Result = await response.json();
4
5  return result;
6}

Ok, sure but it supposed to be a hook... So let's make some adjustments.

We'll use the useState hook to store our response, ensuring that React re-renders our UI when new data is received. If the URL changes, we'll make a new request using the useEffect hook.

1export const useFetch = <Result>(url: string): Result | null => {
2// before data is fetch we let's set data to null to handle gracefully empty state
3  const [data, setData] = useState<Result | null>(null);
4
5  useEffect(() => {
6    // escape if url is falsy
7    if (!url) {
8      return;
9    }
10
11    async function fetchData<Result>(url: string) {
12      const response = await fetch(url);
13      const result: Result = await response.json();
14
15      setData(result);
16    }
17
18    fetchData(url);
19
20  // use dependencies array to make request on each url change
21  }, [url]);
22
23  return data;
24}

Loading state

To enhance user experience in React applications, we should inform users when a request is pending. This can be done using a loading indicator like a bar, spinner, or skeleton loader. Let's add an isFetching flag.

1// Update return type
2export const useFetch = <Result>(url: string): { data: Result | null; isFetching: boolean } => {
3  const [data, setData] = useState<Result | null>(null);
4  const [isFetching, setIsFetching] = useState(false);
5
6  useEffect(() => {
7    if (!url) {
8      return;
9    }
10
11    async function fetchData<Result>(url: string) {
12      // Set flag to true before making a request
13      setIsFetching(true);
14      const response = await fetch(url);
15      const result: Result = await response.json();
16
17      setData(result);
18      // We are awaiting response and parsing it to JSON, so we can safely set flag to false
19      setIsFetching(false);
20    }
21
22    fetchData(url);
23  }, [url]);
24
25  // Return isFetching flag along with data by wrapping them in object
26  return {  data, isFetching };
27}

Network errors

We have to remember that request can also fail because of e.g. network error. To handle it gracefully we will wrap our fetch command into try…catch block and have our network error handle in the catch block.

1// Extend return type with error and look!
2// Now we match our FetchReturnType utility type
3export const useFetch = <Result>(url: string): FetchReturnType<Result> => {
4  const [data, setData] = useState<Result | null>(null);
5  const [isFetching, setIsFetching] = useState(false);
6  // New state for errors
7  const [error, setError] = useState<string | null>(null);
8
9  useEffect(() => {
10    if (!url) {
11      return;
12    }
13
14    async function fetchData<Result>(url: string) {
15      setIsFetching(true);
16
17      // Let's wrap fetching operation and parsing in try/catch block
18      try {
19        const response = await fetch(url);
20        const result: Result = await response.json();
21
22        setData(result);
23        // If something goes wrong we set error state with custom message
24      } catch(error) {
25        setError("Fetching error");
26        // No matter of the result set isFetching to false
27      } finally {
28        setIsFetching(false);
29      }
30    }
31
32    fetchData(url);
33  }, [url]);
34
35  // Don't forget to return error state
36  return {  data, isFetching, error };
37}

Handling failed request

Apart from network issues, server (50*) or client (40*) errors can also occur. We should check the response code and use our error state to inform the user.

1export const useFetch = <Result>(url: string): FetchReturnType<Result> => {
2  const [data, setData] = useState<Result | null>(null);
3  const [isFetching, setIsFetching] = useState(false);
4  const [error, setError] = useState<string | null>(null);
5
6  useEffect(() => {
7    if (!url) {
8      return;
9    }
10
11    async function fetchData<Result>(url: string) {
12      setIsFetching(true);
13
14      try {
15        const response = await fetch(url);
16
17        // Check if response has status code different than 200
18        if (!response.ok) {
19          // Throw error with status text.
20          // You could also throw error here with response.status
21          // or expand this error state to return more information
22          // like error code or more details
23          setError(response.statusText);
24
25          return;
26        }
27
28        const result: Result = await response.json();
29
30        setData(result);
31      } catch(error) {
32        setError("Fetching error");
33      } finally {
34        setIsFetching(false);
35      }
36    }
37
38    fetchData(url);
39  }, [url]);
40
41  return {  data, isFetching, error };
42}

Cancelling http requests

Handling request cancellation if user is leaving page or performing action that makes new request with updated body is the best practice. It will allow us to reduce server traffic and save some serious headaches with unpredictable behaviours connected with promises race.

To do so we will use object that is available in native web api's - Abort Controller. It allows us to register AbortSignal to our request which when emitted will cancel pending request. We will be emitting in useEffect's cleanup function to cancel requests when component unmounts or url changes.

1export const useFetch = <Result>(url: string): FetchReturnType<Result> => {
2  const [data, setData] = useState<Result | null>(null);
3  const [isFetching, setIsFetching] = useState(false);
4  const [error, setError] = useState<string | null>(null);
5
6  useEffect(() => {
7    if (!url) {
8      return;
9    }
10
11    // Create abort controller instance
12    const abortController = new AbortController();
13    async function fetchData<Result>(url: string) {
14      setIsFetching(true);
15
16      try {
17        const response = await fetch(url, {
18          // Extend the fetch options with the abort signal
19          signal: abortController.signal,
20        });
21
22        if (!response.ok) {
23          setError(response.statusText);
24
25          return;
26        }
27
28        const result: Result = await response.json();
29
30        setData(result);
31      } catch(error) {
32        setError("Fetching error");
33      } finally {
34        setIsFetching(false);
35      }
36    }
37
38    // Fetch data if the request is not aborted
39    if (!abortController.signal.aborted) {
40      fetchData(url);
41    }
42
43    // Send abort signal when the component unmounts or the url changes
44    return () => abortController.abort();
45  }, [url]);
46
47  return {  data, isFetching, error };
48}

Outro

That's it! We've created a simple yet effective hook for safely performing HTTP requests with network and request error handling, loading state display, and cancellation support. Feel free to adapt and enhance it for your needs - expand error handling, modify HTTP methods, add request body or headers. You can also expose the fetchData method for on-demand requests. The possibilities are endless! World is yours!

Bonus

As a bonus here are some useful resources: