Fetching Data With A Custom Hook in React

Fetching Data With A Custom Hook in React

Data is an important part of any application. Data is what turns our fancy UIs into functional applications. But the way we go about getting or sending this data is important to ensure our applications continue to perform at optimum levels. Today, we will be looking at how to fetch data with a custom hook in react.

Before we begin, you should ideally have a fair knowledge of the concepts down below

  • How async operations work in javascript

  • Using the useState and useEffect hooks in react

  • Basic understanding of how the fetch API works

  • Shortcircuiting in javascript

If you've got all those, let's get into it.

What is a Custom Hook?

Before we get into building, we need to understand what exactly it is we're even trying to build. A custom hook is simply a reusable function we can call over and over again, anywhere in our react application. When we have some type of logic that we see ourselves rewriting over and over again in different components, we should ideally abstract that into a custom hook we can reuse. Writing a hook is as easy as writing a function in a javascript file and exporting it.

Setting up Our React Project

Since this is of course, a react tutorial, we'll need a react project we can work in. Start by spinning up a new terminal session and navigating to where you would like to create your project.

I'm using command prompt and I've navigated to the projects folder on my desktop where I usually save my projects. Do the same and let's keep going.

Once you're there, create a new react project by running the npx create-react-app command adding what you'd like your new project to be called at the end, like so...

Now that our project is ready, cd into that and open up VSCode by using the command "code .", or, you can open the folder in any other editor of your choice. Now that our project is ready, it's a good idea for us to talk about what we want to achieve.

The Goal

Our goal for this tutorial will be to fetch data from an API and display that data in our app. The API we will be using is JSONPlaceholder, a free, fake REST API. You can check them out here. We will fetch a single todo from the API and display same in our app. Sounds simple right? Let's get into it then.

Creating the Template

We'll start by first setting up the template and styles for our app. You probably already know how to do this so feel free to skip over to the next section if you wish.

What we want to do is quite simple. We want a single container in the middle of our screen where we'll display the todo. Go to the App component and clear out everything in the return statement, then replace it with the code below.

<div className="App">
  <div className="post">
  </div>
</div>

Delete the imports at the top as we won't be needing those and instead import the index.css file.

In the index.css file, clear out everything and replace it with the code below.

*{
  padding: 0;
  margin: 0;
  box-sizing: border-box;
}
.App{
  min-height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  background: #D0E7D2;
}
.post{
  padding: 2em;
  border-radius: 12px;
  background: #79AC78;
}
.error{
   color: red;
}

So that's the template done, time to move on to creating the hook.

Creating the hook

In the src directory, we will create a new directory called hooks. The hooks directory is where we will store any custom hooks we create. Inside hooks, create a new file called "useFetch.js". Appending use to the start of hooks is a react convention and we will stick to that. First thing we'll do is to define a function and then export it. We'll call our function useFetch.

const useFetch = ()=>{
    return "I'm a custom hook!"
}

export default useFetch

And just like that, we've successfully created a custom hook! Let's give it a whirl by importing it in our App component and calling it. In the App component, above the return statement, add the following code:

const fetchResult = useFetch();
console.log(fetchResult);

Remember to import the hook at the very top of the file like so:

import useFetch from "./hooks/useFetch";

Our App.js file should now be looking like this:

Go back to your terminal and run the "npm start" command to spin up a local development server. Once the server is up, your browser should be looking a little something like this:

But we don't care about that. Open up the console and you should be seeing the string "I'm a custom hook!". Our hook is working! That's great, but the hook doesn't do anything remotely useful. Let's fix that by making it fetch us some data.

Fetching data with the hook

We'll be using the hook to help us perform fetch operations. We'll start by making two glaringly wrong assumptions that will make life easier.

  • We will always get the data, there won't ever be any errors

  • Our data will arrive instantly, there's no need for any loading states

With those out of the way, let's get into writing the actual code. inside the useFetch function in the useFetch.js file, clear out our former return statement and add this instead:

const [data, setData] = useState(null);

const getData = async()=>{
  const res = await fetch(url);
  const data = await res.json();
  setData(data);
}
return [getData, data];

Remember to import the useState hook at the very top of the file like so:

import { useState } from "react"

Now let's go over the hook we just created. Firstly, we set a state for the data with an intial value of null. We use state in this hook because going out to fetch data is an asynchronous function which takes some time to do. We won't actually have any data at the initial page render. When our async function completes and we do get the data we want, we would like the components using this hook to rerender with the actual data they need. Hope this makes sense.

The next thing we do is to define an async function, "getData" which does the actual fetch operation to the url provided by the hook. Inside this function, when we get and parse our data, we set this data to the data state we declared at the top using the "setData" function.

The last line, however, is a bit interesting. We return an array containing two values, the "getData" function and the data state. We return these two values so that any component we use our hook in can have access to them when they call the hook. With our hook defined, let's get into actually using it.

Using the custom hook in a component

Remember our goal at the beginning was to fetch a todo from the JSONPlaceholder API and display it in our component. Let's do that now.

First, we want to call our hook with the API url as a parameter and destructure the two values it returns so we can use them in our App component. Clear out everything above the return statement and add this instead:

const url = "https://jsonplaceholder.typicode.com/todos/1";
const [getData, data] = useFetch(url);

Don't forget to import the hook at the top like so:

import useFetch from "./hooks/useFetch";

Now that we have our hook imported, the next thing to think about is when we want to perform the fetch. Ideally, we want to fetch our data immediately our component has mounted. Where do you define tasks you want done after the component has mounted? A useEffect hook with no dependencies! Let's set that up. In the App component, below our useFetch hook, define the useEffect hook like so:

 useEffect(() => {
    getData();
  }, []);

The first argument we pass in is the callback function to be executed everytime the useEffect runs. The second argument is an array of dependencies. Since we only want our fetch to run only once and not depend on any state changes, we pass in an empty array.

The last thing we want to do is to log the data we get to the console so we can verify that everything's working as expected. But remember, our data starts out as null and is only updated after the fetch is completed. Therefore, we'll only want to log our data variable when the fetch has actually completed and it does in fact hold a value. We can do this easily by shortcircuiting. Add this line after the useEffect.

data && console.log(data);

Now that everything is ready, let's save our file and go back to the browser. If you take a look at the console, you should see an object logged to the console. Our hook works! We've now successfully retrieved data from the JSONPlaceholder API and displayed same in the console. Now let's render out the data to the screen itself. This one's the easy part, simply replace our earlier return statement in the App component with this:

return (
    <div className="App">
      {data && <div className="post">{data.title}</div>}
    </div>
  );

Again we use shortcircuiting to ensure the data only renders when we have actual data. Save the file and if you go back to the browser, it should be looking like this:

We've now successfully fetched data from an API and rendered it in our component. That's good but there's a few things we haven't accounted for. Remember at the beginning we made two assumptions? Let's recap what those were:

  • We will always get the data, there won't ever be any errors

  • Our data will arrive instantly, there's no need for any loading states

These assumptions are wrong. We'll always need to account for errors and siince network operations take some time to do, display loading states to the user to communicate that the application is loading the data. Let's do those now.

Adding Error handling to our hook

Errors could occur at any point in the fetch process. Network errors, server errors and the rest all need to be accounted for and handled gracefully in our application. To do this, we'll do two things.

First, we'll add an error state to monitor if any errors have occurred and then we'll wrap the fetch operation in a try/catch block to handle exceptions. Let's do that now. Add an error state to the hook and set its initial value to null like so:

const [error, setError] = useState(null)

Next, we'll wrap the fetch operation in a try block and handle any errors in a catch block. Let's tweak our "getData" function to this:

const getData = async () => {
    setError(null);
    try {
      const res = await fetch(url);
      // if response status is not 200, throw an error.
      if (res.status != 200) {
        throw new Error("Error with getting the data");
      }
      const data = await res.json();
      setData(data);
    } catch (error) {
      setError("An error occurred.");
    }
  };

Now that we've added error handling to our useFetch hook, we'll also return this error state, in addition to the data state, so we can have access to it in any component using the hook.

In the App component, we'll want to also destructure the error state so we can communicate any errors that might have occurred to the user. The useFetch call should now look like this:

const [getData, data, error] = useFetch(url);

Next, we'll want to use shortcircuiting to conditionally render an error state. We'll tweak the return statement of the App component to this:

return (
    <div className="App">
      {data && <div className="post">{data.title}</div>}
      {error && <div className="post error">{error}</div>
    </div>
  );

If you try turning off wifi and refreshing the browser, you should see our error message rendered to the screen. With error handling done, it's time to deal with loading states.

Handling loading states

Network operations usually take some time to do. This time of intermediacy where we don't quite have the data we're looking for but our component is already mounted has to be taken into account. For this demo, we'll simply display a small message telling the user that the data is currently loading. Let's add this behaviour to the hook.

Adding a loading state will be quite similar to how we added an error state earlier. Loading will, however, initially be set to true and once we get the data, or an error, it will be set to false. Let's translate this to code. Add a loading state at the top of the hook like so:

const [loading, setLoading] = useState(true);

Then rewrite the getData function to this:

const getData = async () => {
    setError(null);
    try {
      const res = await fetch(url);
      // if response status is not 200, throw an error.
      if (res.status != 200) {
        throw new Error("Error with getting the data");
      }
      const data = await res.json();
      setData(data);
      setLoading(false);
    } catch (error) {
      setError("An error occurred.");
      setLoading(false);
    }
  };

Remember to add loading to the return statement of the hook so we can use it in our component.

In the App component, we'll destructure the loading state from our hook like so:

const [getData, data, error, loading] = useFetch(url);

We'll then conditionally render a loading state when the fetch operation is still ongoing. Change the return statement of the App component to this:

return (
    <div className="App">
      {data && <div className="post">{data.title}</div>}
      {error && <div className="post error">{error}</div>
      {loading && <div className="post">Loading...</div>
    </div>
  );

With this, we've successfully created a custom hook for fetching data in react with error and loading state handling included. Returning a cleanup function in the useEffect hook to abort the fetch operation when our component is dismounted is still something we could add. This is a bit beyond the scope of this tutorial but you can read more about cleanup functions in react here.

Conclusion

Custom hooks are useful to prevent repetition and also make our react code easier to refactor. This article has gone over the basics of how we can create a simple hook. Thanks for reading to the end and I hope the article helped :)

Happy hacking!