Responding to network events
Another critical piece of any front-end application is network interactions, fetching data, issuing commands, and so forth. Since network communications are an inherently asynchronous activity, we have to rely on events—the EventTarget
interface to be precise.
We'll start by looking at the generic mechanism that hooks up our callback functions with requests and getting responses from the back-end. Then, we'll look at how trying to synchronize several network requests creates a seemingly hopeless concurrency scenario.
Making requests
To interact with the network, we create a new instance of XMLHttpRequest
. We then tell it the type of request that we want to make—GET versus POST and the request endpoint. These request objects also implement the EventTarget
interface so that we can listen for data arriving from the network. Here's an example of what this code looks like:
// Callback for successful network request, // parses JSON data. function onLoad(e) { console.log('load', JSON.parse(this.responseText)); } // Callback for problematic network request, // logs error. function onError() { console.error('network', this.statusText || 'unknown error'); } // Callback for a cancelled network request, // logs warning. function onAbort() { console.warn('request aborted...'); } var request = new XMLHttpRequest(); // Uses the "EventTarget" interface to attach event // listeners, for each of the potential conditions. request.addEventListener('load', onLoad); request.addEventListener('error', onError); request.addEventListener('abort', onAbort); // Sends a "GET" request for "api.json". request.open('get', 'api.json'); request.send();
We can see here that there are a number of possible states for network requests. The successful path is the server responding with the data we need and we're able to parse it as JSON. The error state is when something went wrong, maybe the server isn't reachable. The final state that we're concerned with here is when the request is cancelled or aborted. This means that we no longer care about the successful path because something in our application changed while the request was in flight. The user navigated to another section, for example.
While the previous code was easy enough to use and understand, it's not always the case. We're looking at a single request and a few callbacks. Very seldom do our application components consist of a single network request.
Coordinating requests
In the preceding section, we saw what the basic interaction with XMLHttpRequest
instances looks like for making a network request. The challenge surfaces when there are several requests. Most of the time, we make multiple network requests so that we have the data necessary for rendering a UI component. The responses from the back-end will all arrive at different times, and are likely dependent on one another.
Somehow, we need to synchronize the responses of these asynchronous network requests. Let's take a look at how we can go about doing this using the EventTaget
callback functions:
// The function that's called when a response arrives , // it's also responsible for coordinating responses. function onLoad() { // When the response is ready, we push the parsed // response onto the "responses" array, so that we // can use responses later on when the rest of them // arrive. responses.push(JSON.parse(this.responseText)); // Have all the respected responses showed up yet? if (responses.length === 3) { // How we can do whatever we need to, in order // to render the UI component because we have // all the data. for (let response of responses) { console.log('hello', response.hello); } } } // Creates our API request instances, and a "responses" // array used to hold out-of-sync responses. var req1 = new XMLHttpRequest(), req2 = new XMLHttpRequest(), req3 = new XMLHttpRequest(), responses = []; // Issue network requests for all our network requests. for (let req of [ req1, req2, req3 ]) { req.addEventListener('load', onLoad); req.open('get', 'api.json'); req.send(); }
There's a lot of extra bits to consider when there's more than one request. Since they all arrive at different times, we need to store the parsed responses in an array, and with the arrival of every response, we need to check if we have everything we expect. This simplified example doesn't even take into consideration failed or cancelled requests. As this code alludes, the callback function approach to synchronization is limiting. In the coming chapters, we'll learn how to overcome this limitation.