While this tutorial has content that we believe is of great benefit to our community, we have not yet tested or edited it to ensure you have an error-free learning experience. It's on our list, and we're working on it! You can help us out by using the "report an issue" button at the bottom of the tutorial.

Introduction

A custom hook is a JavaScript function with a unique naming convention that requires the function name to start with use and has the ability to call other hooks.

The idea behind custom hooks is to extract component logic in to reusable functions.

Often times as we build out React applications, we see ourselves writing almost the same exact codes in two or more different components. Ideally what we could do in such cases would be to extract that recurrent logic into a reusable piece of code (hook) and reuse it where needed.

Before hooks, we shared stateful logic between components using render props and higher order components, however, since the introduction of hooks and since we came to understand how neat they make these concepts, it no longer made sense to keep using those. Basically, when we want to share logic between two JavaScript functions, we extract it to a third function possibly because both components and hooks are equally just functions.

Abstracting Fetch into useFetch()

Compared to using the native fetch API out of the box, abstracting it into the useFetch hook gives it a one-liner ability, more declarative code style, reusable logic, and overall cleaner code. Take a look at this useFetch example:

const useFetch = (url, options) => { const [response, setResponse] = React.useState(null); useEffect(async () => { const res = await fetch(url, options); const json = await res.json(); setResponse(json); }); return response; };

Here, the effect hook, called useEffect , preforms two major functions:

Fetches the data with the native fetch API. Sets the data in the local state of the component with the state hook’s update function.

You will also notice that the promise resolving happens with async/await.

Avoiding Reference Loops

The effect hook runs on two occasions—when the component mounts and updates. This means that if nothing is done about the useFetch example above, you will run into a recurrent loop cycle. This is because you are setting the state after every data fetch. As a result, the component updates and the effect runs again when the state is set.

This will result in an infinite data fetching loop. To resolve this, you should only fetch data when the component mounts. Provide an empty array as second argument to the effect hook, as this will stop it from activating on component updates but only when the component is mounted.

useEffect(async () => { const res = await fetch(url, options); const json = await res.json(); setResponse(json); }, []); // empty array

The second is an array containing all the variables that the hook depends on. If any of the variables change the hook runs again. If the argument is an empty array, the hook doesn’t run when updating the component since there are no variables to watch.

useEffect ’s Return Error

You may have noticed that you are using async/await to fetch data in the effect hook. However, according to documentation stipulations, every function annotated with async returns an implicit promise. So in your effect hook, you are returning an implicit promise whereas an effect hook should only return either nothing or a clean up function.

By design, you are already breaking this rule because the code is not returning nothing, and a promise does not clean up anything.

If you use the code as is, you will get a warning in the console pointing out the fact that the useEffect function must return a cleanup function or nothing.

To summarize, using async functions directly in the useEffect() function is frowned upon. You can resolve this by writing and using the async function inside of the effect.

React.useEffect(() => { const fetchData = async () => { const res = await fetch(url, options); const json = await res.json(); setResponse(json); }; fetchData(); }, []);

Instead of using the async function directly inside the effect function, we created a new async function fetchData() to perform the fetching operation and simply call the function inside useEffect. This way, we abide by the rule of returning nothing or just a cleanup function in an effect hook. And if you should check back on the console, you won’t see any more warnings.

Handling Errors

One thing we haven’t covered so far is how we can handle error boundaries in this concept. When using async/await, it is common practice to use the try/catch construct for error handling which will also work here.

const useFetch = (url, options) => { const [response, setResponse] = React.useState(null); const [error, setError] = React.useState(null); React.useEffect(() => { const fetchData = async () => { try { const res = await fetch(url, options); const json = await res.json(); setResponse(json); } catch (error) { setError(error); } }; fetchData(); }, []); return { response, error }; };

Here, you used JavaScript try/catch syntax to set and handle error boundaries. The error itself is just another state initialized with a state hook. Whenever the hook runs, the error state resets. However, whenever there is an error state, the component renders feedback to the user. This allows you to perform any desired operation with it.

Setting Loading Indicators

You can use hooks to handle loading states for your fetching operations. It is another state variable managed by a state hook. This means that if you wanted to implement a loading state in the last example, you’ll set the state variable and update the useFetch() function accordingly.

const useFetch = (url, options) => { const [response, setResponse] = React.useState(null); const [error, setError] = React.useState(null); const [isLoading, setIsLoading] = React.useState(false); React.useEffect(() => { const fetchData = async () => { setIsLoading(true); try { const res = await fetch(url, options); const json = await res.json(); setResponse(json); setIsLoading(false) } catch (error) { setError(error); } }; fetchData(); }, []); return { response, error, isLoading }; };

Demonstrating the Concepts with an App

This step will cover a hands-on demonstration to put these concepts into practice. To do this, you will build an app that will fetch dog images and their names. You’ll use useFetch to call the Dog API for this app’s data.

First, define your useFetch() function. You will reuse the example created while demonstrating error handling.

const useFetch = (url, options) => { const [response, setResponse] = React.useState(null); const [error, setError] = React.useState(null); React.useEffect(() => { const fetchData = async () => { try { const res = await fetch(url, options); const json = await res.json(); setResponse(json); } catch (error) { setError(error); } }; fetchData(); }, []); return { response, error }; };

Next, create the App() function that will use the useFetch() function to request the dog data:

function App() { const res = useFetch("https://dog.ceo/api/breeds/image/random", {}); if (!res.response) { return <div>Loading...</div> } const dogName = res.response.status const imageUrl = res.response.message return ( <div className="App"> <div> <h3>{dogName}</h3> <div> <img src={imageUrl} alt="avatar" /> </div> </div> </div> ); }

In this example you passed the URL to the useFetch() function with an empty options object to fetch the data for the dog. Once the data is fetched, the code extracts it from the response object and displays it on screen. You can review a demonstration on CodeSandbox here.

Conclusion