How to create a React Hook to make AJAX calls
Tags: react, hooks, webdev, javascript
Published: 2020-06-11 13:58:18


Today we are going to create a simple hook that helps me everyday in my React projects, both web and react-native: a hook to make Ajax Calls and that returns the response. 

For testing the hook, we are going to build a simple app that will display all the Houses of Game Of Thrones, provided by https://www.anapioficeandfire.com. 



To summarize, this is what we are going to do in this article:

- create a new React Hook
- this Hook will accept an URL to fetch and a series of options (queries, method and body)
- this Hook will return an object with the AJAX response and a loading and error boolean values  
- Every time one of the options given to the hook is changed, the Hook will fetch again the URL 
- create a demo app to test this useFetch Hook 



**Let's start** ✨





## First, let's create the skeleton app ☠️

I think I made this step 300 times in the past years, but I always find myself googling the correct command to use with create-react-app. I think I have some sort of selective forgetfulness for this simple command... so this part is more for the future me than for you :) 

```bash
npx create-react-app use-fetch
cd use-fetch
yarn start
```



And after installing all the right modules, we go to https://localhost:3000 and the app is running :) 

![Screenshot 2020-04-15 at 15.17.15](https://dev-to-uploads.s3.amazonaws.com/i/953cyla8twnua86irxps.png)




## Create the hook

Let's start by creating a folder in  **src** called  **hooks**  and create inside a file called **useFetch.js** .

```bash
mkdir src/hooks
touch src/hooks/useFetch.js
```



And inside the file we will put this:

```javascript
import { useState, useEffect } from "react";

const queryString = (params) =>
  Object.keys(params)
    .map((key) => `${key}=${params[key]}`)
    .join("&");

const createUrl = (url, queryOptions) => {
  return url + "?" + queryString(queryOptions);
};

export default (url, options = { body: {}, query: {} }) => {
  const [data, setData] = useState({
    response: null,
    error: false,
    loading: true,
  });

  useEffect(() => {
    setData({ ...data, error: null, loading: true });
    fetch(createUrl(url, options.query), {
      method: options.method || "GET",
      headers: {
        "Content-Type": "application/json",
      },
      body: options.method !== "GET" && JSON.stringify(options.body),
    })
      .then(async (response) => {
        const data = await response.json();
        setData({
          response: data,
          error: !response.ok,
          loading: false,
        });
      })
      .catch((error) => {
        //fetch throws an error only on network failure or if anything prevented the request from completing
        setData({
          response: { status: "network_failure" },
          error: true,
          loading: false,
        });
      });
  }, [url, JSON.stringify(options)]);

  return data;
};


```



Let's take a look together at the code of our hook. There are two utility function that I'm not going to explain here, but if you need any help you can always [contact me](https://twitter.com/urcoilbisurco) and ask :)



We are going to explore the hook part by part:

```javascript
export default (url, options = { method: "GET", body: {}, query: {} }) => {

....
})
```

 The hook will accept 2 parameters: 

- an URL
- a 'options' object, that inside will have 
  - a HTTP method (GET, POST)
  - a body, if you are going to use the method POST
  - a query, where you are going to put all the query params of the AJAX call.

**Important**: I specified only GET and POST methods. This is because this hook is made only to **fetch** data, not to update/create resources. Normally you should always use GET requests to fetch data, but since some APIs in the wide Internet are also using POST requests I decided to add that as well.

```javascript
export default (url, options = { method: "GET", body: {}, query: {} }) => {
	const [data, setData] = useState({
    response: null,
    error: false,
    loading: true,
  });
....
}) 
```

We are going to use the hook **useState** to store some internal variables, that at the end of the hook will be returned to the React component. We are going to initialize the state with an object with 3 parameters: 

- Response, that will contain the JSON response of the API called
- Error, in case the response status is not ok
- Loading, that will be true if the hook is still fetching the request. Since we are going to call the request as the next step, loading is already set to true



### Inside useEffect

Let's continue to explore the hook. Here we are going to use the hook **useEffect** to do something only when something in the params changes; if the component changes the url or any of the params inside options (query, body, method), the **useEffect** function will re-run. 

```javascript
useEffect(() => {
    setData({ response: data.response, error: false, loading: true });
		...
}, [url, JSON.stringify(options)]);
```

We are using JSON.stringify to return a string of our **options** values. In this way the useEffect won't have any problem noticing changes even if the object is nested.

The first thing that we are going to do is to change the value of the **data** state with:

- loading set at true
- error set at false
- response will still be the previous response (null for the first time). This will help if you want to display the old data even when you are fetching the new data.

### Fetch for the rescue 🚀

We are going to use the **fetch** function to make the AJAX call. We are going to add the header **Content-Type** to **application/json** since we are going to use only APIs that requests json parameters.  

Just a note: instead of throwing an error if the response is not ok (like axios), fetch is still resolving successfull, but will have a response.ok set to **false**. For this reason we will need to check in the resolved data if response.ok is true or false and set the **error** state field accordily. 

```javascript
useEffect(() => {
    setData({ ...data, error: false, loading: true });
    fetch(createUrl(url, options.query), {
      method: options.method || "GET",
      headers: {
        "Content-Type": "application/json",
      },
      body: options.method !== "GET" && JSON.stringify(options.body),
    })
      .then(async (response) => {
        const data = await response.json();
        setData({
          response: data,
          error: !response.ok,
          loading: false,
        });
      })
      .catch((error) => {
        //fetch throws an error only on network failure or if anything prevented the request from completing
        setData({
          response: { status: "network_failure" },
          error: true,
          loading: false,
        });
      });
  }, [url, JSON.stringify(options)]);
```

Every time the fetch method resolve or throws an error, we are going to update the data state with all the right fields, setting **loading** to false.

### And... that's it!

This is everything about the hook, now we just need to use it 🚀





## Use the useFetch hook

We will use the "An API of Ice and Fire" https://www.anapioficeandfire.com/ to create a simple paginated app that shows all the different Houses in the "A Song of Ice and Fire" series.


![An API of Ice and Fire](https://dev-to-uploads.s3.amazonaws.com/i/4xsrg1en7a2v8eyqbxk6.png)


NB: all the code can be found on [my Github page](https://github.com/urcoilbisurco/useFetchDemoApp). As you can see, I removed some unused files from the boilerplate create-react-app. Also note that this is the final result, at the end of this article.

Let's go to **src/App.js** and replace the content with this:

```react
import React from "react";
import useFetch from "./hooks/useFetch";
import "./App.css";

function App() {
  const { response, error, loading } = useFetch(
    "https://www.anapioficeandfire.com/api/houses",
    {
      query: {
        page: 1,
        pageSize: 10,
      },
    }
  );

  if (loading) {
    return <div className="loading">Loading...</div>;
  }
  if (error) {
    return <div className="error">{JSON.stringify(error)}</div>;
  }
  return (
    <div className="App">
      {response.map((data) => {
        return (
          <div className="datapoint" key={data.Date}>
            <h3>{data.name}</h3>
            {data.words && <cite>"{data.words}"</cite>}
            {data.coatOfArms && (
              <p>
                <b>Coat of Arms: </b>
                {data.coatOfArms}
              </p>
            )}
          </div>
        );
      })}
    </div>
  );
}

export default App;


```





And this will be the result.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/mbpfnv7x277zek09bjgc.png)

We haven't add any style yet, so it's pretty **ugly**. We can fix that by adding some CSS in **src/App.css** (we won't use any fancy styled-components or scss module or any of the things that the cool kids are using these days since it's only a demo).

```css
body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
    "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;

  font-size: 20px;
}


h1,
h3,
p,
button {
  padding: 0;
  margin: 0;
  font-size: inherit;
}

h1 {
  padding: 16px 32px;
  font-size: 28px;
  color: #666;
}

p,
cite {
  font-size: 16px;
}


.datapoint {
  padding: 16px 32px;
  border-bottom: 2px solid #9dc8c8;
  font-size: 20px;
  color: #666;
}

```





**That's much better!** 

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/sl1zlawg635q3jpqv313.png)

## Support pagination (and queries to useFetch)

So right now we are showing only 10 houses. That's ok, but I think that we can do better. We are gonna change the code to add some buttons to go to the next (or previous) page and view new results ✨



## But first, add some style

Let's add some extra style that we will need in the next steps: open src/App.css and replace the content with this:

```css
body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
    "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  font-size: 20px;
}

h1,
h3,
p,
button {
  padding: 0;
  margin: 0;
  font-size: inherit;
}

h1 {
  padding: 16px 32px;
  font-size: 28px;
  color: #666;
}

p,
cite {
  font-size: 16px;
}

.datapoint {
  padding: 16px 32px;
  border-bottom: 2px solid #9dc8c8;
  font-size: 20px;
  color: #666;
}

.pagination {
  margin-top: 15px;
  padding: 0 32px;
}

button {
  outline: none;
  padding: 10px 16px;
  appearance: none;
  border: 2px solid #519d9e;
  background: #519d9e;
  color: white;
  font-weight: 600;
  border-radius: 8px;
  margin-right: 16px;
}

.loading {
  min-height: 400px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 32px;
  color: #519d9e;
  font-weight: 800;
}

```



### Use useState to handle the currentPage variable

We are going to use a **currentPage** variable to be aware of what is the current page shown in the app, so let's setup that in our **src/App.js**

 ```react
import React, { useState } from "react";
import useFetch from "./hooks/useFetch";
import "./App.css";

function App() {
  const [currentPage, setCurrentPage] = useState(1);
  const { response, error, loading } = useFetch(
    "https://www.anapioficeandfire.com/api/houses",
    {
      query: {
        page: currentPage,
        pageSize: 5,
      },
    }
  );
....


 ```

We initialise the value of currentPage to 1 and we also edited the **page** value of the useFetch query object to use currentPage instead of the constant 1 of before.

Now, let'add some extra parts in the JSX. We are going to:

- add a title, with the current page number inside;
- add under the list of Houses the pagination section, with the 2 buttons to change page;
- move the Loading div, so the title and the pagination section will always be visible.

```react
return (
    <div className="App">
      <h1>Game of Thrones Houses - Page {currentPage}</h1>
      {loading && <div className="loading">Loading page {currentPage}</div>}
      {!loading &&
        response.map((data) => {
          return (
            <div className="datapoint" key={data.Date}>
              <h3>{data.name}</h3>
              {data.words && <cite>"{data.words}"</cite>}
              {data.coatOfArms && (
                <p>
                  <b>Coat of Arms: </b>
                  {data.coatOfArms}
                </p>
              )}
            </div>
          );
        })}
      <div className="pagination">
        {currentPage > 1 && (
          <button
            onClick={() => {
              setCurrentPage(currentPage - 1);
            }}
          >
            Go to page {currentPage - 1}
          </button>
        )}
        <button
          onClick={() => {
            setCurrentPage(currentPage + 1);
          }}
        >
          Go to page {currentPage + 1}
        </button>
      </div>
    </div>
  );
```



And... we are ready! Let's try it on localhost:3000 



![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/iqdos90wfx6s2ds8il65.gif)



Let review what we have done today:

- created a new React Hook ✔️
- this Hook will accept an URL to fetch and a series of options (queries, method and body)
- this Hook will return an object with the AJAX response and a loading and error boolean values   ✔️
- Every time one of the options given to the hook is changed, the Hook will fetch again the URL ✔️
- create a demo app to test this useFetch Hook ✔️



We can still do better. In the next weeks I will release a new tutorial that will enhance useFetch to:

- automatically transform the response
- conditionally call the AJAX call (now it's calling it immediately)
- add a default response (useful if you don't want to call the API immediately)
- add support for redux and dispatch



As always, [message or follow me on Twitter](https://twitter.com/urcoilbisurco) if you have any questions 💛