This tutorial is out of date and no longer maintained.
A custom hook is a JavaScript function that has the ability to call other hooks. The idea behind custom hooks is to extract component logic to reusable functions.
Often times as we build out React applications, we end up writing almost the same exact codes in two or more different components. Ideally, what we could do in such cases would be to extract the common 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, 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.
To complete this tutorial, you’ll need:
This tutorial was verified with Node v16.6.1, npm
v7.20.3, react
v17.0.2, and react-scripts
v4.0.3.
useFetch()
Compared to using the native fetch API, abstracting it to the useFetch
hook gives it a one-liner ability, reusable logic, more declarative code style, and overall cleaner code.
Here is the useFetch
example:
const useFetch = (url, options) => {
const [response, setResponse] = React.useState(null);
React.useEffect(async () => {
const res = await fetch(url, options);
const json = await res.json();
setResponse(json);
});
return response;
};
Here, the effect hook, called useEffect
, performs two major functions:
You will also notice that the promise resolving happens with async/await
.
The effect hook runs on two occasions — when the component mounts and updates.
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 the second argument to the effect hook, as this will stop it from activating on component updates but only when the component is mounted:
const useFetch = (url, options) => {
const [response, setResponse] = React.useState(null);
React.useEffect(async () => {
const res = await fetch(url, options);
const json = await res.json();
setResponse(json);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // empty array
return response;
};
The second is an array containing all the variables that the hook depends on.
Note: Your project may have ESLint rules that conflict with using an empty array.
OutputReact Hook React.useEffect has missing dependencies: 'options' and 'url'. Either include them or remove the dependency array. (react-hooks/exhaustive-deps)
However, specifying options
will create an infinite loop when options
is an empty object. This is due to how JavaScript handles comparisons of objects. You may wish to omit it, recreate the object, or memoize the object.
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
ErrorsYou 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.
OutputWarning: An effect function must not return anything besides a function, which is used for clean-up.
It looks like you wrote useEffect(async () => ...) or returned a Promise. Instead write the async function inside your effect and call it immediately:
useEffect(() => {
async function fetchData() {
// You can await here
const response = await MyAPI.getData(someId);
// ...
}
fetchData();
}, [someId]); // Or [] if effect doesn't need props or state
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.
const useFetch = (url, options) => {
const [response, setResponse] = React.useState(null);
React.useEffect(() => {
const fetchData = async () => {
const res = await fetch(url, options);
const json = await res.json();
setResponse(json);
};
fetchData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return response;
};
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 clean-up function in an effect hook.
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();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
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.
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 };
};
Now, the isLoading
state is tracked.
This step will cover a hands-on demonstration to put these concepts into practice. To accomplish 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.
You can use create-react-app
to quickly scaffold a React application. This will generate react
, react-dom
, and react-script
dependencies. It will also build public/index.html
and src/index.js
files.
First, define your useFetch()
function. You will reuse the example created while demonstrating error handling.
import React from "react";
import ReactDOM from "react-dom";
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();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
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="Random Dog Image" />
</div>
</div>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
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 the screen.
In this tutorial, you made a small demo to see how you can declaratively fetch data and render it on screen by using the useFetch
hook with the native fetch()
API.
Data fetching has always been an issue to contend with when building frontend applications.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!