Stop using XMLHttpRequest and switch to fetch

The most popular XHR API is XMLHttpRequest which wasn’t really made for what we’ve been using it for. That’s why fetch API has been created, the fetch API is in some sort a modern replacement for XMLHttpRequest. Let’s have a basic look at this window.fetch method.

Browser compatibility

An important thing while developing a web application is the browser compatibility with the technologies used. As XMLHttpRequest is older it logically have a better compatibility with older browsers compared to fetch however, there is well done fetch polyfill’s which makes this modern API compatible with older browsers such as IE, etc…

(c.f XMLHttpRequest Compatibility, Fetch Compatibility)

Basic XMLHttpRequest Usage

XHR is a bit overcomplicated in my opinion and I still don’t understand why XML is uppercase while Http is camel-cased, it doesn’t make any sense at all. Anyways, here’s a common XHR usage

if (window.XMLHttpRequest) { // Mozilla, Safari, etc...
    request = new XMLHttpRequest();
} else if (window.ActiveXObject) { // IE
    try {
        request = new ActiveXObject('Msxml2.XMLHTTP');
    }
    catch (e) {
        try {
            request = new ActiveXObject('Microsoft.XMLHTTP');
        }
        catch (e) {}
    }
}

// Open the request and send it.
request.open('GET', 'https://example.com/api', true);
request.send(null);

Of course JavaScript frameworks make XHR more pleasant to work with, but what you see above is a “simple” example of the most basic XHR usage. XHR is a real mess.

Basic fetch Usage

The fetch function is provided in the global window scope, with the first argument being the URL (required) and the second the options (optional)

// fetch(url, options) | url: required - options: optional
fetch('https://example.com/api', {
    method: 'get'
}).then(function(response) {
    // Success :)
}).catch(function(err) {
    // Error :(
});

And as you can see fetch uses Javascript Promises in order to handle results/callbacks. If you aren’t used to Javascript Promises yet, get used to it – it will soon be everywhere.

Request Headers

The ability to set request headers is important in request flexibility, you can work with request headers by executing new Headers()

// Create an empty Headers instance
const headers = new Headers();

// Add headers
headers.append('Custom-Header', 'MySuperValue');
headers.append('Content-Type', 'text/html');

// Check if this header is present
headers.has('Content-Type'); // true
headers.has('Some-Header'); // false

// Get the value of a specific header
headers.get('Custom-Header'); // MySuperValue

// Set a new value for an existing header
headers.set('Content-Type', 'text/plain');

// Delete a header
headers.delete('Custom-Header');

// Add initial values
const headers = new Headers({
    'Content-Type': 'application/json',
    'User-Agent': 'MySuperUserAgent'
});

In order to use request headers, you must first create a new Request instance

const request = new Request('https://example.com/api', {
    headers: new Headers({
        'Content-Type': 'application/json',
        'User-Agent': 'MyCustomUserAgent'
    })
});

fetch(request).then(function(response) {
    // process the response
}).catch(function(error) {
    // process the error
});

Request

A Request instance represents the request piece of a fetch call. By passing fetch a Request you can make advanced and customized requests:

  • method - GET, HEAD, POST, PUT, DELETE
  • url - URL of the request
  • headers - associated Headers object
  • referrer - referrer of the request
  • mode - cors, no-cors, same-origin
  • credentials - should cookies go with the request? omit, same-origin
  • redirect - follow, error, manual
  • integrity - subresource integrity value
  • cache - cache mode (default, reload, no-cache)

Here’s a sample of Request usage

// Build the request
const request = new Request('https://example.com/anything', {
    method: 'HEAD',
    mode: 'no-cors',
    redirect: 'follow',
    headers: new Headers({
        'Content-Type': 'text/html'
    })
});

// And now use the request
fetch(request).then(function() {
    // handle response
});

Only the first parameter, the URL, is required. Each property becomes read only once the Request instance has been created. Also important to note that Request has a clone method which is important when using fetch within the Service Worker API – a Request is a stream and thus must be cloned when passing to another fetch call.

fetch('https://example.com/anything', {
    method: 'HEAD',
    mode: 'no-cors',
    redirect: 'follow',
    headers: new Headers({
        'Content-Type': 'text/html'
    })
}).then(function() {
    // handle response
});

You’ll likely only use Request instances within Service Workers since the Request and fetch signatures can be the same.

Response

The fetch’s then method is provided a Response instance but you can also manually create Response objects yourself – another situation you may encounter when using service workers. With a Response you can configure:

  • type - basic, cors
  • url
  • useFinalURL - Boolean for if url is the final URL
  • status - status code (ex: 200, 404, etc.)
  • ok - Boolean for successful response (status in the range 200-299)
  • statusText - status code (ex: OK)
  • headers - Headers object associated with the response.
// Fake response for service worker testing -- new Response(body, options)
const response = new Response('response body', {
    ok: false,
    status: 404,
    url: '/'
});

// The fetch's then gets a Response instance back
fetch('https://exemple.com/').then(function(response) {
    console.log('ok: ', response.ok); // false
});

The Response also provides the following methods:

  • clone() Creates a clone of a Response object
  • error() Returns a new Response object associated with a network error
  • redirect() Creates a new response with a different URL
  • arrayBuffer() Returns a promise that resolves with an ArrayBuffer
  • blob() Returns a promise that resolves with a Blob
  • formData() Returns a promise that resolves with a FormData object
  • json() Returns a promise that resolves with a JSON object
  • text() Returns a promise that resolves with a USVString (text)

Handling JSON

Let’s say you make a request for JSON, the resulting callback data has a json method for converting the raw data to a JavaScript object

fetch('https://example.com/api/list.json').then(function(response) {
    // Convert to JSON
    return response.json();
}).then(function(jsObj) {
    // jsObj is an javascript object from the json response
    console.log(jsObj);
});

The json() method is a simple shortcut to JSON.parse(jsonString)

Handling Basic Text/HTML Responses

JSON isn’t always the desired request response format so here’s how you can work with an HTML or text response

fetch('/404').then(function(response) {
    return response.text();
}).then(function(htmlresponse) {
    // <!DOCTYPE ....
    console.log(htmlresponse);
});

You can get the response text via chaining the Promise’s then method along with the text() method.

Handling Blob Responses

For example, loading image via fetch its a bit different

fetch('https://example.com/someimage.jpg').then(function(response) {
    return response.blob();
})
.then(function(imageBlob) {
    document.querySelector('img').src = URL.createObjectURL(imageBlob);
});

The blob() method of the Body mixing takes a Response stream and reads it to completion.

Posting Form Data

AJAX is used a lot for sending form data, here’s how you would do it with the use of fetch

fetch('https://example.com/submit', {
    method: 'post',
    body: new FormData(document.getElementById('myForm'))
});

And if you want to post some JSON data

fetch('https://example.com/submit', {
    method: 'post',
    body: JSON.stringify({
        some: document.querySelector('#some').value,
        json: document.querySelector('#json').value,
        data: document.querySelector('#data').value
    })
});

Simple as that!

Polyfill

There is a lot of Polyfill’s for fetch method, but I highly suggest you to check the GitHub one.