useFetch — A Custom React Hook

One of the most common React Hooks that I’ve found myself using is one to handle API calls. Often we see ourselves writing almost the same exact codes in more than one different component. 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 the need be.

I am going to use Fetch API for the HTTP calls. You can use axios as well, it’s up to you.

  • Loading Indicator
  • Error Handling
  • Using On didMount or Some After
  • Dynamic Url
  • Dynamic Parameters
  • Manual Refetch

Without using a custom hook, you would probably do something similar to this:

import React, { useState, useEffect } from 'react'const Foo = () => {
const url = 'some url'
const [data, setData] = useState(null)
useEffect(() => {
const fetchData = async () => {
fetch(url)
.then((response) => response.json())
.then((result) => {
setData(result)
})
}
fetchData()
}, [])
if(!data) return nullreturn (
<div>
{data.map((item, itemIndex) => (
...
))}
</div>
)
}
export default Foo

To convert this example to a custom hook, it would look like this:

import { useState, useEffect } from 'react'const useFetch = (url) => {
const [data, setData] = useState(null)
useEffect(() => {
const fetchData = async () => {
fetch(url)
.then((response) => response.json())
.then((result) => {
setData(result)
})
}
fetchData()
}, [])
return { data }
}
export default useFetch

This is a very basic example that has many issues; not including handling errors, loading indicator, etc. It’s already quite a bit a few lines of code and is obviously not reusable.

Our component could then call it like this:

const url = 'some url'
const { data } = useFetch(url)

Let’s start to add the features I mentioned above.

The loading indicator is a must in my opinion. Because knowing the fetch process still goes on, we can show a loading component or an icon or something else for nice user experience.

Let’s define a state for loading to achieve that.

import { useState, useEffect } from 'react'const useFetch = (url) => {
const [data, setData] = useState(null)
const [isLoading, setIsLoading] = useState(false)

useEffect(() => {
const fetchData = async () => {
setIsLoading(true)
fetch(url)
.then((response) => response.json())
.then((result) => {
setData(result)
setIsLoading(false)
})
}
fetchData()
}, [])
return { data, isLoading }
}
export default useFetch

Our component could use it like this:

const url = 'some url'
const { data, isLoading } = useFetch(url)
if (isLoading) return <LoadingComponent />

Here is another must feature for our custom hook. We’ll define 2 states, one is a boolean value that indicates whether has an error or not and another one is a string value that includes the error message.

We’ll be also using the try/catch syntax to set and handle error boundaries. It would look like this:

import { useState, useEffect } from 'react'const useFetch = (url) => {
const [data, setData] = useState(null)
const [isLoading, setIsLoading] = useState(false)
const [hasError, setHasError] = useState(false)
const [errorMessage, setErrorMessage] = useState('')
useEffect(() => {
const fetchData = async () => {
setIsLoading(true)
try {
const response = await fetch(url)
const result = await response.json(
if (response.ok) {
setData(result)
} else {
setHasError(true)
setErrorMessage(result)

}
setIsLoading(false)
} catch (err) {
setHasError(true)
setErrorMessage(err.message)

setIsLoading(false)
}
}
fetchData()
}, [])
return { data, isLoading, hasError, errorMessage }
}
export default useFetch

Our component could use it like this:

const url = 'some url'
const { data, isLoading, hasError, errorMessage } = useFetch(url)
if (hasError) return <ErrorComponent message={errorMessage} />

What if we would like to use this hook some after the component renders rather than didMount stage. To achieve this, we can use a prop (let’s say: skip) to skip the data fetching function.

We’ll define it false by default to use fetching on didMount stage.

import { useState, useEffect } from 'react'const useFetch = (url, skip = false) => {
const [data, setData] = useState(null)
const [isLoading, setIsLoading] = useState(false)
const [hasError, setHasError] = useState(false)
const [errorMessage, setErrorMessage] = useState('')
useEffect(() => {
const fetchData = async () => {
if (skip) return
setIsLoading(true)
try {
const response = await fetch(url)
const result = await response.json()
if (response.ok) {
setData(result)
} else {
setHasError(true)
setErrorMessage(result)
}
setIsLoading(false)
} catch (err) {
setHasError(true)
setErrorMessage(err.message)
setIsLoading(false)
}
}
fetchData()
}, [])
return { data, isLoading, hasError, errorMessage }
}
export default useFetch

Our component could use it like this:

const url = 'some url'
const { data, isLoading, hasError, errorMessage } = useFetch(url, (skip = true))

What if we would like to change the url dynamically to trigger the fetching process again? Why not? Let’s implement it.

We’ll change the url prop name to initialUrl to use it as a state. The initialUrl prop will be the default value of the url state.

We’ll add url state to the dependency of the useEffect hook to trigger it again. Then we’ll export its setState function which is updateUrl to use it from our component.

import { useState, useEffect } from 'react'const useFetch = (initialUrl, skip = false) => {
const [url, updateUrl] = useState(initialUrl)
const [data, setData] = useState(null)
const [isLoading, setIsLoading] = useState(false)
const [hasError, setHasError] = useState(false)
const [errorMessage, setErrorMessage] = useState('')
useEffect(() => {
const fetchData = async () => {
if (skip) return
setIsLoading(true)
try {
const response = await fetch(url)
const result = await response.json()
if (response.ok) {
setData(result)
} else {
setHasError(true)
setErrorMessage(result)
}
setIsLoading(false)
} catch (err) {
setHasError(true)
setErrorMessage(err.message)
setIsLoading(false)
}
}
fetchData()
}, [url])
return { data, isLoading, hasError, errorMessage, updateUrl }
}
export default useFetch

Our component could use it like this:

const url = 'some url'
const { data, isLoading, hasError, errorMessage, updateUrl } = useFetch(url)
if(...) updateUrl('some another url')

What about parameters? And what about changing it dynamically to use it like filtering or something else? Sounds great. Let’s implement this too.

We’ll take parameters as a prop which will be an object. And then, we’ll transpile them with encodeURIComponent to use properly. Following that, we’ll add params state to the dependency of the useEffect hook to trigger it again. It would look like this:

import { useState, useEffect } from 'react'const useFetch = (initialUrl, initialParams = {}, skip = false) => {
const [url, updateUrl] = useState(initialUrl)
const [params, updateParams] = useState(initialParams)
const [data, setData] = useState(null)
const [isLoading, setIsLoading] = useState(false)
const [hasError, setHasError] = useState(false)
const [errorMessage, setErrorMessage] = useState('')
const queryString = Object.keys(params)
.map((key) => encodeURIComponent(key) + '=' +
encodeURIComponent(params[key])).join('&')
useEffect(() => {
const fetchData = async () => {
if (skip) return
setIsLoading(true)
try {
const response = await fetch(`${url}${queryString}`)
const result = await response.json()
if (response.ok) {
setData(result)
} else {
setHasError(true)
setErrorMessage(result)
}
setIsLoading(false)
} catch (err) {
setHasError(true)
setErrorMessage(err.message)
setIsLoading(false)
}
}
fetchData()
}, [url, params])
return { data, isLoading, hasError, errorMessage, updateUrl, updateParams }
}
export default useFetch

Our component could use it like this:

const url = 'some url'const { data, isLoading, hasError, errorMessage, updateUrl, updateParams } = useFetch(
url,
(initialParams = {
id: 123456789,
query: 'Lorem Ipsum'
})
// will be transpiled as 'id=123456789&query=Lorem%20Ipsum'
)
if(...) {
updateParams({
id: 4815162342,
query: 'Dharma Initiative'
})

// will be transpiled as 'id=4815162342&query=Dharma%20Initiative'
}

Another common thing I’ve needed is the ability to manually trigger the API call again. A good example would be a page with a list of resources and want to refresh the list from a callback after creating a new resource.

To do this we’ll simply keep a number in the state that’s a dependency on the useEffect, that we increment every time we want to force a refresh.

Finally, it would look like this:

import { useState, useEffect } from 'react'const useFetch = (initialUrl, initialParams = {}, skip = false) => {
const [url, updateUrl] = useState(initialUrl)
const [params, updateParams] = useState(initialParams)
const [data, setData] = useState(null)
const [isLoading, setIsLoading] = useState(false)
const [hasError, setHasError] = useState(false)
const [errorMessage, setErrorMessage] = useState('')
const [refetchIndex, setRefetchIndex] = useState(0)
const queryString = Object.keys(params)
.map((key) => encodeURIComponent(key) + '=' +
encodeURIComponent(params[key])).join('&')

const refetch = () => setRefetchIndex((prevRefetchIndex) => prevRefetchIndex + 1)
useEffect(() => {
const fetchData = async () => {
if (skip) return
setIsLoading(true)
try {
const response = await fetch(`${url}${queryString}`)
const result = await response.json()
if (response.ok) {
setData(result)
} else {
setHasError(true)
setErrorMessage(result)
}
setIsLoading(false)
} catch (err) {
setHasError(true)
setErrorMessage(err.message)
setIsLoading(false)
}
}
fetchData()
}, [url, params, refetchIndex])
return { data, isLoading, hasError, errorMessage, updateUrl, updateParams, refetch }
}
export default useFetch

It could be then used in a way similar to this:

const url = 'some url'const { data, isLoading, hasError, errorMessage, updateUrl, updateParams, refetch } = useFetch(url)return <button onClick={refetch} />

Have you noticed that we’re setting isLoading state in response.ok, its else and catch cases? Let’s use finally syntax for better development. It’ll look like this:

import { useState, useEffect } from 'react'const useFetch = (initialUrl, initialParams = {}, skip = false) => {
const [url, updateUrl] = useState(initialUrl)
const [params, updateParams] = useState(initialParams)
const [data, setData] = useState(null)
const [isLoading, setIsLoading] = useState(false)
const [hasError, setHasError] = useState(false)
const [errorMessage, setErrorMessage] = useState('')
const [refetchIndex, setRefetchIndex] = useState(0)
const queryString = Object.keys(params)
.map((key) => encodeURIComponent(key) + '=' +
encodeURIComponent(params[key])).join('&')
const refetch = () => setRefetchIndex((prevRefetchIndex) => prevRefetchIndex + 1)useEffect(() => {
const fetchData = async () => {
if (skip) return
setIsLoading(true)
try {
const response = await fetch(`${url}${queryString}`)
const result = await response.json()
if (response.ok) {
setData(result)
} else {
setHasError(true)
setErrorMessage(result)
}
} catch (err) {
setHasError(true)
setErrorMessage(err.message)
} finally {
setIsLoading(false)
}

}
fetchData()
}, [url, params, refetchIndex])
return { data, isLoading, hasError, errorMessage, updateUrl, updateParams, refetch }
}
export default useFetch

In this post, I have explained and made a small demo to show how we can declaratively fetch data and render it on screen by using the useFetch hook with the native Fetch API.

If you are using Next.js, I highly recommend you to use SWR by Vercel instead of this custom hook.

Originally published at Onur Şuyalçınkaya Blog.

The Startup

Get smarter at building your thing. Join The Startup’s +785K followers.

Sign up for Top 10 Stories

By The Startup

Get smarter at building your thing. Subscribe to receive The Startup's top 10 most read stories — delivered straight into your inbox, once a week. Take a look.

By signing up, you will create a Medium account if you don’t already have one. Review our Privacy Policy for more information about our privacy practices.

Check your inbox
Medium sent you an email at to complete your subscription.

Onur Şuyalçınkaya

Written by

Frontend Engineer, JavaScript enthusiast, DJ, writer and minimalist. Develops things at heycar — JS, React, React Native, Next.js.

The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +785K followers.

Onur Şuyalçınkaya

Written by

Frontend Engineer, JavaScript enthusiast, DJ, writer and minimalist. Develops things at heycar — JS, React, React Native, Next.js.

The Startup

Get smarter at building your thing. Follow to join The Startup’s +8 million monthly readers & +785K followers.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store