Now I know what you are thinking... puppies are cute right?
No, Okay.
It's a little overkill to always reach out for a third-party library to handle data fetching for simple use cases when you have a fetch
. axios
and apollo-client
are terrific libraries for handling requests. I've used them and I love them too. The purpose of this article is to show you how you an alternative way you can make the requests using fetch
. I was mind-blown by some of the concepts I learnt in the process of writing this article. Some of the shortcomings of fetch
are: it doesn't support network interceptors and won't work well if your web application is server side rendered without isomorphic-unfetch
.
Before you install a package to help you make requests, let me show you some of the nifty features fetch
has to offer.
A quick history lesson - XMLHttpRequest
Before fetch
became a standard, we had XMLHttpRequest
. No, it had nothing to do with fetching only XML from the server. It works with any type of data being sent to or from a server. It works both asynchronously or synchronously. This is because JavaScript is single-threaded and you don't want to block the main thread. Your web application will be unusable and whoever will review your code will get a little riled up and probably hunt you down. Please don't do that.
I should clarify that XMLHttpRequest
is still supported in all browsers. Caveat, I've used this XMLHttpRequest
twice. First time when I was learning how to make network requests in Js and at the time this article was being written๐ .
I found a cave painting of how a request is made using XMLHttpRequest
. It looks something like this:
let request = new XMLHttpRequest()
request.open('GET', 'http://random-url-on-the-internet.lol', true)
request.onload = () => {
let data = JSON.parse(this.response)
console.log(data)
}
request.onerror = () => {
// handle non-HTTP errors e.g. losing connection
console.log(`Error occured: ${request.status}`)
}
request.send()
This makes my head hurt every time I look at it. It's probably what inspired Matt Zabriskie to author axios
. It can be a little tedious creating a new instance of XMLHttpRequest
every time you wish to make a request. Keep in mind that, we haven't set headers or tried out other types of requests.
There are a couple more methods provided by XMLHttpRequest
such as abort()
, and setRequestHeader()
. You can explore them in the MDN Documentation
So, fetch eh?
Since I've shown you what a network request using XMLHttpRequest
looks like, here's how it looks like using Fetch()
const request = async () =>
await fetch('http://random-url-on-the-internet.lol')
.then(res => res.json())
.then(console.log)
.catch(console.error)
request()
Looks fairly easy, right? ๐
We have created an arrow function request()
that is async
. request()
returns a Promise and we have to await
it as well, just to make sure we don't block the main thread running on the browser.
The first argument is the URL to your API. By default, all requests made are 'GET'. More on how to make a 'POST' in the next section. The second argument, which is optional is an object containing the details of the request, such as the method, headers, cors policy and content-type.
.then()
method is chained to the request because it is a Promise. This means once the request is complete, we execute something. In our case, we convert the response to JSON. The second .then()
logs the data to the console. If there is an error exception .catch()
will capture it.
Fetch
is supported in all major browsers, except IE. Why won't you just accept your fate IE?
Request metadata
Fetch
accepts a second parameter, the request options that is an object. It allows you to control a number of settings such as request headers, body, cors and cache. Let's look at an example where we make a 'POST' request, attach a token to the Authorization header and set the content type to application/json
:
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer xxxxx-token-here'
}
}
const request = async () =>
await fetch('http://random-url-on-the-internet.lol', options)
.then(res => res.json())
.then(console.log)
.catch(console.error)
request()
If you would like to look into more options, MDN takes a deep dive into using Fetch
.
Fetch from REST APIs
This is probably the simplest of the bunch and it will seem intuitive. I used jsonplaceholder.typicode.com API to demonstrate how to make network requests. Some APIs may require you attach an API key or a token to the request. The examples provided should give you a solid background on how to use fetch
effectively.
GET requests
'GET' are pretty straightforward since
const requestSomeData = () => {
fetch('https://jsonplaceholder.typicode.com/posts/1')
.then((response) => response.json())
.then((json) => console.log(json))
}
requestSomeData()
POST requests
Create an options object in which you will specify the method is 'POST' and set the request body. Depending on the API you are using, you will probably need to send the body in JSON format.
const options = {
method: 'POST',
body: JSON.stringify({
title: 'A Fresh Start',
body: 'Maybe it is time you should consider of switching careers',
userId: 1,
}),
headers: {
'Content-type': 'application/json; charset=UTF-8',
}
}
const postSomeData = () => {
fetch('https://jsonplaceholder.typicode.com/posts', options)
.then((response) => response.json())
.then((json) => console.log(json))
}
If you would like to make PUT, PATCH or DELETE requests, all you will need to do is specify the method in the request options
Fetch from GraphQL APIs
GraphQL requests are HTTP requests. Requests made to a GraphQL API are POST
requests. Set the content type to application/json
.
For the examples below, I created a sample GraphQL API hosted on Codesandbox. The data is stored in memory.
If you would like to fork it and play around with it, you can find it here. The API will allow you to request for books, create and books.
Queries
Queries define the information a client sends to a server, describing what they need.
Define the query and include it in the request body in JSON.
const url = 'https://3l097.sse.codesandbox.io/'
const GET_BOOKS = `
query {
books {
id
title
author
published
}
}`
const querySomeData = () => {
fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ GET_BOOKS })
})
.then(res => res.json())
.then(({ data }) => console.log(data))
.catch(error => {
console.log('Something happened.!๐', error)
})
}
querySomeData()
Mutations
Mutations are responsible for modifying data in a GraphQL API. Similar to what POST
, PUT
and DELETE
do in a REST API.
Define your mutation and add variables that would represent data captured from a form, for example. A mutation allows you define the data you would like to be returned once its execution is complete.
const url = 'https://3l097.sse.codesandbox.io/'
const CREATE_BOOK = `
mutation($title: String!, $author: String!, $description: String!) {
createBook(
title: $title,
author: $author
description: $description
){
id
title
author
description
}
}`
const mutateSomeData = () => {
fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: CREATE_BOOK,
variables: {
title: "I'm already tired of Fetch",
author: "Ruheni Alex",
description: "You should read this one"
}
})
})
.then(res => res.json())
.then(console.log)
.catch(console.error)
}
mutateSomedata()
I highly encourage you to inspect the requests in the network tab using the browser devtools to understand what is going on under the hood.
Fetch on Window Focus
I never knew one could request data by focusing on a tab or window. Turns out it has nothing to do with fetch. But it's a pretty neat feature to include in your application.
This is especially helpful when a user leaves your application and data gets stale. When the user gets back to your application, data will be fetched and existing
const fetchSomeData = () => {
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then(response => response.json())
.then(json => console.log(json))
}
window.addEventListener('focus', fetchSomeData)
Fetch Retries
Let's face it. Requests are bound to fail at some point. You can improve the user experience in your web application by making the request a couple more times before informing the user what went wrong. It's going to be a recursive function that will call itself until it runs out of retries.
const retryFetch = async (url, options = {}, retries = 5) => {
const response = await fetch(url, options)
.then(res => {
if (res.ok) return res.json()
if (retries > 0) {
return retryFetch(url, options, retries - 1)
} else {
throw new Error(res)
}
})
.catch(console.error)
return response
}
Fetch Wrapper
You can make a custom fetch
function that would work for all types of requests. This is a concept I learnt from Kent C. Dodds. Now, my example isn't be polished, but I'm sure you can customize and add whatever would tickle your fancy.
const customFetch = (url, { body, ...customConfig }) => {
const headers = {
'Content-Type': 'application/json'
}
if (body) {
return config.body = JSON.stringify(body)
}
const config = {
method: body ? 'POST' : 'GET',
...customConfig,
headers: {
...headers,
...customConfig.headers
}
}
return window.fetch(url, config)
.then(async res => {
const data = await res.json()
if (res.ok) {
return data
} else {
return Promise.reject(data)
}
})
}
export { customFetch }
Cancelling requests
Turns out, you can cancel a request. Yeah, I didn't know about it too. I came across this feature as I was reading the react-query docs. At first, I thought it was a library specific feature, but after some research, it's natively supported in the browsers. It's fairly new to me and I may make a lot of mistakes but feel free to explain it further to me.
Why do you need this? You don't. Fetch
returns a promise which has 3 states: fulfilled
, rejected
and pending
. There is no way you can cancel an ongoing fetch
. It comes in handy when a user decides an action isn't needed anymore.
First, create a controller instance from AbortController()
. controller
has a single method, abort()
and one property signal
that allows you to set an event listener to it. signal
is then added to the request options. In the example below, I created a timer to invoke abort()
method after 100ms. This will throw an error to the console.
Note this is still an experimental technology.
const controller = new AbortController();
const signal = controller.signal;
let url = 'https://jsonplaceholder.typicode.com/todos/1'
setTimeout(() => controller.abort(), 100);
const fetchSomeData = () => {
fetch(url, { signal })
.then(res => res.json())
.then(data => console.log(data))
.catch(error => {
if (error.name = 'AbortError') {
console.log('You just aborted a fetch!๐')
}
})
}
fetchSomeData()
Learn more
Replace axios with a custom fetch wrapper by Kent C. Dodds. In this article
Cover Photo by Rob Fuller on Unsplash
Top comments (17)
I have to desagree on some points because axios is a much more powerfull request layer.
All these points explain why so many of us prefer using a higher layer library like axios.
Hi! I totally agree with you.
I've noticed the tone I used was a little condescending. Sorry about that. I've made an update clarifying the intention of writing the article and some of the shortcomings of fetch. ๐
Didn't mean to rattle you๐
Hello Alex,
No worries. I was not bothered at all by the tone of your post.
You have written a good article, with a lot of insights for developpers.
Keep it up.
Yeh agreed. Fetch isn't a replacement at all for axios, axios simply provides so much more than just fetching something. +1 for interceptors I was going to mention but you beat me too it :)
Cave painting๐๐. Woow!
Yep, there is a reason why SSR frameworks still favor Nuxt. It's hell with fetch, there's is always calls responding weirdly.
How does isormophic-unfetch perform in SSR using Nuxt?
That's a good question! Never tried it, but seems very attractive. Very minimalist.
Zapper has a nice approach: replacing fetch or this.fetch as needed (being a compiler it's easy enough to do). Effectively, you're just writing you're only using fetch.
Nice article but beware of the
response.json()
call before checkingreponse.ok
, it may get you or anyone copying your example in trouble!I found this video by Joel Thoms to explain clearly the issue: youtube.com/watch?v=aIboXjxo-w8
Learnt something new today! Thank you for sharing
cross-fetch would be worth considering these days github.com/lquixada/cross-fetch
There's one specific case I'd like to point out where XMLHttpRequest could be preferred, and is regarding cancellations.
When you abort a fetch call, you are aborting the network call, but the Promise itself is not canceled because Promises don't support cancelation. As you can see, the actual behavior is that the promise throws.
This is fine for most cases and practically all product development, but in very specific and extreme circumstances this might not be desired because Promise ticks are actually expensive and can clog the stack, which can end up having a consequence in the execution flow of the program.
A callback-oriented API that doesn't rely on Promises doesn't have this drawback, and that just wouldn't be possible to do with fetch, but is possible to do with XMLHttpRequest.
Thanks I was actually using axios yesterday and wondering how to replace with fetch.
Why don't you open the post with dogs are cute instead of cows are cute? Otherwise it seems like a distraction
I'm glad you liked it. Sure, I'll make the update ๐
Wow that's amazing!
How about interceptors?
Does fetch support it?
No. Fetch is very minimal and not a replacement for axios at all. Unless you are just grabbing a few things
So glad to see this so well received by the community!