loading...

React: Creating a Custom Hook for Fetching Data

admantium profile image Sebastian Updated on ・3 min read

Fetching data from an external or internal API is a common use case for web applications. With react functional components, there are different hooks to fetch data. This post explains these hooks, and helps you to understand when to use them.

Context: Fetching a User’s Board Game Collection

In my app, I want to read the board game collection of a user and render it. The platform BoardGameGeek offers a JSON API. Here is an example:

curl https://bgg-json.azurewebsites.net/collection/newuser
[
  {
    "gameId": 180263,
    "name": "The 7th Continent",
    "image": "https://cf.geekdo-images.com/original/img/iQDBaRJ2LxJba_M7gPZj24eHwBc=/0x0/pic2648303.jpg",
    "thumbnail": "https://cf.geekdo-images.com/thumb/img/zj6guxkAq2hrtEbLGFrIPCh4jv0=/fit-in/200x150/pic2648303.jpg",
    [...]
  }
]

Requirements

Before starting to code, I like to spend some time thinking about the requirements. This way, you have a rough outline and a checklist for evaluating how far you are in your implementation.

Let’s brainstorm. Fetching data is a process that needs an unknown amount of time. Therefore, we should give the process a timeout, and track the state of loading. Fetching can produce different errors: It can fail completely, or the dataset might be different than we expect or has errors itself. We should handle these error cases, and we should consider error as a final state of the fetching process.

The essential requirements are:

  • R1 it should be configurable with a url and timeout
  • R2 it should return the states of loading, error and result

Basic Implementation

The essential requirements can be fulfilled with the following code:

1 import React, {useState} from 'react';
2
3 function useFetchData(url, timeout) {
4   const [data, setData] = useState([]);
5   const [loading, setLoading] = useState(false);
6   const [error, setError] = useState(false);
7
8   return {data, loading, error};
9 }
  • In line 3, we define the useFetchData function, the constructor which is named according to custom hook convention and receives the values url and timeout
  • In line 4 - 6, the variables data, loading and error are defined with the useState hook
  • In line 8, all state variables are returned

Now we need to implement the required functions.

Fetching the Data

Let’s write the function that fetches the data.

1  async function load() {
2    setLoading(true);
3    try {
4      const result = await axios.fetch(url, {timeout: timeout}).data;
5      setData(result);
6    } catch (e) {
7      setError(true);
8    }
9    setLoading(false);
10 }
  • In line 2, we set the loading = true, and only at the end to this function we set it to false
  • In line 3, we use a try … catch block surrounding the actual API call to catch all errors
  • In line 4, we use the axios library to make the actual request to the URL, and provide the timeout value
  • In line 5-7, If fetching the data is successful, we set the data to the result, and if it is not successful, we set error = true

With this logic, we ensure that data fetching always has a well-defined state: It is loading, or if it’s not loading, it has a result or an error.

Refactoring

The hook fulfills our requirements R1 and R2. What can we improve? Whenever the component is called we should reset its state to the initial values.

function init() {
  setData([]);
  setLoading(true);
  setLoading(false)
}

async function load() {
  init();
  ...
}

What would happen if we just call the load function inside the function component declaration? The function will change the state of the component, which triggers a re-render, which would execute load again, and …

So, the function needs to be called from the outside - we need to export it to the component that uses this hook.

return {data, loading, error, load};

Final Component

Here is the final component:

import React, {useState} from 'react';

function useFetchData(url, timeout) {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(false);

  function init() {
    setData([]);
    setLoading(true);
    setLoading(false)
  }

  async function load() {
    init();
    setLoading(true);
    try {
      const result = await axios.fetch(url, {timeout: timeout}).data;
      setData(result);
    } catch (e) {
      setError(true);
    }
    setLoading(false);
  }

return {data, loading, error, load};
}

export default useFetchData;

Conclusion

This article showed how to implement a custom fetch data hook. We learned that the component always needs to possess a precise state: Its loading, or it is done loading with either a result or an error. When the API is accessed, we assume that the request can fail, that the data is not validated and other errors - all of them captured and taken care of. Lastly, we export all state variables and the load function so that the caller has maximum control.

Posted on Jun 1 by:

admantium profile

Sebastian

@admantium

IT Project Manager & Developer

Discussion

markdown guide
 

My sugesstion:

The hook should take a promise as param. It will more flexible than take url

const useFetchData = (promiseFn) => {
...
const invoke = async () => {
...
const res = await promiseFn()
...
}

You can use fetch, axios, or any HTTP client library.

Fetch ex.:

const response = useFetchData(fetch('url', { method: 'POST' }).then(res => res.json))

I prefer return type same as React.useState api.

return [{ data, loading, error }, invoke]

More easier for naming.
Alot of case, you will need more than 2 api calls. So you can:

const [api1, api1InvokeFn] = useFetchData()
const [api2, api2InvokeFn] = useFetchData()

React.useEffect(() => {
// call api1 when mounted
api1InvokeFn()
}, [api1InvokeFn])

<button onClick={api2InvokeFn}>Fetch</button>

if (api1.loading) {
... show loading indicator
}
if (!api2.loading && api2.data) {
... do something data
}
 

Using promises improves the flexibilty of your app, in my case I'm happy to stay with the synchronous call and having a timeout barrier.

 

Awesome!

What about the case where you make an async request and immediately switch to another screen? React will complain that it cannot re-render an unmounted component or something like that. Is there a way to cancel the request?

 

I think this is a question for Thành Trang?

 

Shouldn't the load function be in a useCallback hook? When you call it from a useEffect hook, you must add the load function into the dependency array. If you add it, that causes an infinite loop as the load function gets recreated on every render.



const load = useCallback(async () => {
    init();
    setLoading(true);
    try {
      const result = await axios.fetch(url, {timeout: timeout}).data;
      setData(result);
    } catch (e) {
      setError(true);
    }
    setLoading(false);
}, [url])
 

Nice read Sabastian!

I recently inherited a project that had something similar, and, it took me quite a bit of time to get familiar with the magic it had abstracted. Your's looks a lot easier to follow to say the least.

swr.now.sh is also a great alternate that takes care of some additional magic if anyone reading doesn't feel like maintaining their own fetch hook.

 

Hi Jamie, that's a good link you provided. Indeed there are several fetch hooks on Github and other sources. My article is primary a tutorial to get into hooks and understand them. Once familiar, more complex implementations and examples become accessible.

 
 

Hi Jospeh, I don't yet publish code on Github, but you can see the final component code above.