The React hook API means we can reuse our code, more efficiently than ever before. A common function of any modern web application is to fetch data from a remote source, typically a REST API
. Let's combine both of these, and create a reusable custom React hook that can be used to pull data from any REST API
.
What you will learn
- Usage of create-react-app via npx
- Composition of a custom React hook
- Usage of the
useState
hook - Usage of the
useEffect
hook - Usage of the
useRef
hook - To create a reusable useFetch hook
Set up the project
The fastest way to get any react project up and running is by using the create-react-app package. We'll use it via npx
rather than worrying if we have the latest version installed:
npx create-react-app my-custom-hook
create-react app
bundles a project together, using webpack, react, babel and react-testing library. Pretty much everything we are going to need. If you fancy doing it manually, I have a post that explains that. But for now, create-react-app is perfect for our needs.
cd
into the root of the project:
cd my-custom-hook
code .
Will open in your favourite IDE, (mine is vscode, but it doesn't matter which you use), and replace the contents of ./src/App.js
with:
import React from 'react'
function App() {
return <h1>useFetch()</h1>
}
export default App
At this point, its always worth, starting the app and opening your developer console
and checking that the app runs and without any errors:
yarn && yarn start
Considerations
-
response - Our custom hook wants to be a function, that accepts two arguments, although only the first argument is required. The first argument is the
URL
of the remote source, and it should be astring
. The second argument, if present is theheader
and so should always be anobject
. This header can contain information like setting themethod
toPOST
,UPDATE
,DELETE
etc., setting thebody
, supplyingtokens
etc. When the data is return, it should be anobject
. -
error - We need to consider what happens if the server is down and there is an error. An error
object
should be returned. -
isLoading - We should also note that requesting an external data source such as a
REST_API
is asynchronous, and so we need to be able to handle theload-state
(whether it is loading or is loaded). Due to its nature, it should be aboolean
.
Just so we are aware of what we are trying to achieve, type the following into our terminal:
curl 'https://jsonplaceholder.typicode.com/users/5'
This URL is for a REST_API
when we make an HTTP
GET
request to it, it returns a data object. It is this object that we expect our hook to return when we are finished.
Create the required API
When creating hooks like this, I prefer to reverse engineer them. So I am going to start with how I want our custom hook to work. Replace ./src/App.js
again, but this time with:
import React from 'react'
import { useFetch } from ',/useFetch'
const App = () => {
const [response, error, isLoading] = useFetch(
`https://jsonplaceholder.typicode.com/users/5`
)
if (isLoading) {
return <h1>Loading..</h1>
}
if (error) {
console.log(error.message)
}
return (
<>
<pre>response: {JSON.stringify(response, null, 2)}</pre>
</>
)
}
export default App
Let us discuss this code change.
import { useFetch } from ',/useFetch'
Imports the hook from its source, we haven't created this yet, but do not worry.
const [response, error, isLoading] = useFetch(
`https://jsonplaceholder.typicode.com/users/5`
)
This code is our API. This pattern is how we want our custom hook to work. For this example, I'm only going to use the first argument and directly enter the URL
string.
We then want to deconstruct the three considerations that we drew up before response
, error
and isLoaded
.
I am deconstructing these from an array
rather than an object
. Meaning the destructured variables are not name bound
so I could use this naming convention:
const [userData, userError, userLoading] = useFetch(
`https://jsonplaceholder.typicode.com/users/5`
)
This is important if we want to use the useFetch hook twice in the same component (i.e., grab the users' location data and then grab the weather data based on that location data). By using it more than once, there would be a naming conflict if we deconstructed from an
object
. (attempting to reassign the constant variableresponse
, multiple times with multiple responses). Because of this, the order of this deconstruction is critical. For example, we cannot request just the[response, isLoaded]
as could be done with object deconstruction, because the information in theisLoaded
placeholder would be theerror
information (the second item in the returned array[response, error, isLoaded]
).
We can then handle each of our considerations. The error
conditional allows us to handle the error. The isLoading
conditional allows us to handle the case of when the data is still loading, for example, on a slower, older mobile device. Use cases for loading states tend to be progress bars
, or spinners
.
Only if there there are no errors and the loading has been completed will the we hit our final return statement:
return (
<>
<pre>response: {JSON.stringify(response, null, 2)}</pre>
</>
)
Because this is a demo, all we are returning is a React.Fragment which displays the response
object, using two spaces for the tab.
So, now our project won't run. And we haven't even started on the hook! It is fine; we have already achieved quite a lot; we know the shape of the API from our three considerations, we also identified handling multiple use cases in the same component.
Let us start with (from the root of the project):
cd src
touch useFetch.js
Inside ./src/useFetch.js
we can mock our hook out, by using the considerations we made earlier:
//./src/useFetch.js
import React from 'react'
const useFetch = (url, options) => {
const response = { data: 'HELLO WORLD!' }
const error = false
const isLoading = false
return [response, error, isLoading]
}
export { useFetch }
Let's see our mock in action, return to the root directory in the terminal and start the application:
cd ..
yarn && yarn start

So the mock works, and we've made excellent progress. Now let us make some more considerations:
-
useState - We are going to utilize the useState hook, to store our three variables in the components state.
-
useEffect - We want to make use of the useEffect hook, it allows us to run a function, and define a dependencies array. We can place the two arguments that the initial function call receives (the URL and maybe the options) inside this dependency array. That way, the hook triggers every time it receives a new URL. This API is essential if we expect to use the hook multiple times in the same component.
-
useRef - useEffect can also return a callback function.When supplied, it does this as the component is unmounted from the DOM. This callback function is for clean up. In our instance, we can use the
useRef
hook as a placeholder for the components mounted state. Doing this allows us to record the mounting and un-mounting of this component, and thus preventing memory leaks. -
fetch API - We will use the fetch method to make an HTTP GET request, passing its response into our components state.
-
response - We want to create the object to be returned inside the response. As stated before, it must be an object, and we can use it to attach other, helpful information to the response object.
-
error - if the fetch API receives an error, it is this error that is passed into our components state and then returned as an error object.
-
isLoading - pass it into the components state, and then return the value as a boolean.
Lets edit ./src/useFetch.js
taking all of these new considerations into account:
import React from 'react'
const useFetch = (url, options) => {
const isMounted = React.useRef(true)
const [response, setResponse] = React.useState({})
const [error, setError] = React.useState(false)
const [isLoading, setIsLoading] = React.useState(false)
React.useEffect(() => {
if (isMounted.current) {
if (!url) {
return
}
setIsLoading(true)
const fetchData = async function () {
const response = await fetch(url, options)
.then((res) => res.json())
.then((jsonData) => {
setIsLoading(false)
setResponse({
'end-point': url,
status: 200,
error: false,
'data-type': Array.isArray(jsonData) ? 'array' : typeof jsonData,
'data-length': jsonData.length,
data: jsonData,
})
})
.catch((err) => {
setIsLoading(false)
setError({ error: true, message: err })
})
}
fetchData()
}
return () => {
isMounted.current = false
}
}, [url, options])
return [response, error, isLoading]
}
export { useFetch }
Let us discuss this code change.
const isMounted = React.useRef(true)
The useRef
hook will create a ref
object called isMounted
and set its current
property to true (isMounted.current
). We'll use this in a moment.
const [response, setResponse] = React.useState({})
const [error, setError] = React.useState(false)
const [isLoading, setIsLoading] = React.useState(false)
We define our three variables as pieces of component state, using the useState
hook.
React.useEffect(() => {})
We then declare a useEffect
hook.
React.useEffect(() => {
if (isMounted.current) {
// ..
}
})
Immediately inside of this useEffect
, we open a conditional based on the ref
object we just created (isMounted
). We know it is currently true because we just set it.
This conditional statement means we can only do anything in this useEffect
hook while it is true.
React.useEffect(() => {
if (isMounted.current) {
// ..
return () => {
isMounted.current = false
}
}
})
We can now return a callback function from the useEffect
hook (i.e. when the component unmounts). In that callback, we can set isMounted.current
to false. It is now not possible to fetch data when the component is unmounted. Doing this prevents a common error while fetching data, called memory leaks.
React.useEffect(() => {
if (isMounted.current) {
if (!url) {
return
}
// ..
return () => {
isMounted.current = false
}
}
})
Next, we use another conditional, this time checking the URL. If there is no URL, we return, and the hook does nothing. This conditional allows us to use our hook multiple times in the same component By using a ternary operator in the fetch call like this:
const [weatherData, weatherError, weatherIsLoading] = useFetch(
coords.lat && coords.long
? `http://api.openweathermap.org/data/2.5/weather?lat=${coords.lat}&lon=${coords.long}&APPID=${WEATHER_API_KEY}&units=metric`
: null
)
const [forecastData, forecastError, forecastIsLoading] = useFetch(
weatherData.data
? `http://api.openweathermap.org/data/2.5/forecast/daily?id=${weatherData.data.id}&appid=${WEATHER_API_KEY}`
: null
)
I saw in a video from Kent C. Dodds, and its a genuinely versatile pattern.
React.useEffect(() => {
if (isMounted.current) {
if (!url) {
return
}
setIsLoading(true)
// ..
return () => {
isMounted.current = false
}
}
})
We then set isLoading
to true.
React.useEffect(() => {
if (isMounted.current) {
if (!url) {
return
}
setIsLoading(true)
const fetchData = async function () {
// ..
}
fetchData()
return () => {
isMounted.current = false
}
}
})
Because we cannot make the useEffect use an async function itself, we must define an async
function, and immediately invoke it.
React.useEffect(() => {
if (isMounted.current) {
if (!url) {
return
}
setIsLoading(true)
const fetchData = async function () {
return await fetch(url, options)
.then((res) => res.json)
.then((jsonData) => {
setIsLoading(false)
setResponse({
'end-point': url,
status: 200,
error: false,
'data-type': Array.isArray(jsonData) ? 'array' : typeof jsonData,
'data-length': jsonData.length,
data: jsonData,
})
})
.catch((err) => {
setIsLoading(false)
setError({ error: true, message: err })
})
}
fetchData()
return () => {
isMounted.current = false
}
}
})
We then use the fetch API, parse the response, and assign the parsed data to our response object. None of this is necessary; you could just return the parsed data. I like the additional data, and seeing as this is a reusable component, I hope to catch other use cases further down the road.
And that is it. A fully functional, totally reusable custom React hook. Like all good developers, we push the code to git hub.
The full project can be seen here