DEV Community

Cover image for How to create a React Hook to make AJAX calls
francesco marassi
francesco marassi

Posted on

How to create a React Hook to make AJAX calls

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 :)

npx create-react-app use-fetch
cd use-fetch
yarn start
Enter fullscreen mode Exit fullscreen mode

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

Create the hook

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

mkdir src/hooks
touch src/hooks/useFetch.js
Enter fullscreen mode Exit fullscreen mode

And inside the file we will put this:

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;
};


Enter fullscreen mode Exit fullscreen mode

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 and ask :)

We are going to explore the hook part by part:

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

....
})
Enter fullscreen mode Exit fullscreen mode

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.

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

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.

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

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.

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)]);
Enter fullscreen mode Exit fullscreen mode

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

NB: all the code can be found on my Github page. 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:

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;


Enter fullscreen mode Exit fullscreen mode

And this will be the result.

Alt Text

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).

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;
}

Enter fullscreen mode Exit fullscreen mode

That's much better!

Alt Text

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:

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;
}

Enter fullscreen mode Exit fullscreen mode

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

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,
      },
    }
  );
....


Enter fullscreen mode Exit fullscreen mode

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.
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>
  );
Enter fullscreen mode Exit fullscreen mode

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

Alt Text

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 if you have any questions 💛

Top comments (0)