When I first started learning RxJS, I could instinctively see that observable streams offered all kinds of possibilities in solving many of the problems I encountered day to day in front end web application development. I’d already been using the flux architecture for a while, and had been blown away by the clarity of organisational structure and separation of concerns it brought to my web apps. I’d read that RxJS could do the same, and was keen to learn about how. The elegant handling of HTTP requests seemed like the obvious starting point for this learning journey.
However, I quickly became frustrated at how little information I could find in one place regarding good practices around this topic (error handling in particular), and had to find out most things piecemeal though reading around, a lot of browsing Stackoverflow and Github issue threads, and personal experimentation. This article serves as a catalogue for what I have learned so far.
I’m going to explain some handy ways of doing the following:
- Creating observable streams from HTTP requests
- Handling HTTP error response
- The elegant handling of out of order HTTP request completion
- Throttling user input
- Some bonus extra tips an tricks
I’m going to assume some knowledge of the absolute basics of creating and subscribing to observables, as this is easy to find online, and is where I was at when I started experimenting with RxJS and HTTP requests.
To demonstrate all of these techniques we’ll create a example mini app that consumes github user api. It will enable the user to type a github username in a box and if valid, display their avatar underneath. I’ll use many variations of the app to demonstrate the different ways of using RxJS. To keep things simple the app only uses RxJS, bootstrap and a little bit of jQuery to glue the page together.
Note: the github user api, has a rather pathetic rate limit of 60 requests per hour if used unauthenticated. So if you get too trigger happy with the examples, they might stop working for a bit.
The Setup
To kick us off, let’s create a text input, and create an observable from its ‘keyup’ event.
<div class="container">
<!-- Search controls -->
<h1>Search Github Users</h1>
<div class="form-group">
<label for="search-box">
Username:
</label>
<input
id="search-box"
type="text"
class="form-control"
/>
</div>
<button
id="search-button"
class="btn btn-primary"
>
Search
</button>
</div>
let userClicksSearchButton = Rx.Observable.fromEvent(
$("#search-button"),
"click"
)
.map((event) => {
return $("#search-box").val();
});
userClicksSearchButton.subscribe((searchTerm) => {
alert(searchTerm);
});
Example 1: Input Box
View the live example in codepen
Type in the box and hit the search button to see an alert containing the text input value. Notice the extra
map
chained after the fromEvent
observable. This enables us to map the current input value of search input box to our userClicksSearchButton
observable, replacing the default event object that would normally be emitted.OK, we now have an observable we can work with that will emit the user’s input when they hit the search button. To fire off our HTTP request, we’re going to create an observable stream we can subscribe to using our userClicksSearchButton as a source:
<div class="container">
<!-- Search controls -->
<h1>Search Github Users</h1>
<div class="form-group">
<label for="search-box">
Username:
</label>
<input
id="search-box"
type="text"
class="form-control"
/>
</div>
<button
id="search-button"
class="btn btn-primary"
>
Search
</button>
<hr />
<!-- Search result -->
<a
href=""
target="_blank"
id="search-result"
style="display:none;"
>
<h2 id="search-result__login"></h2>
<img
src=""
alt="avatar"
width="150px"
height="150px"
id="search-result__avatar"
/>
</a>
</div>
let userClicksSearchButton = Rx.Observable.fromEvent(
$("#search-button"),
'click'
)
.map((event) => {
return $("#search-box").val();
});
userClicksSearchButton
.flatMap((searchTerm) => {
return Rx.Observable.fromPromise(
$.get('https://api.github.com/users/' + searchTerm)
);
})
.subscribe((response) => {
renderUser(
response.login,
response.html_url,
response.avatar_url
);
});
function renderUser(login, href, imgSrc) {
let searchResult = $("#search-result");
searchResult.show();
searchResult.attr("href", href);
$("#search-result__avatar").attr('src', imgSrc);
$('#search-result__login').text(login);
}
Example 2: Button Search
View the live example in codepen
Have a go at typing your Github username into the box and hitting search, you should see your avatar appear underneath. Be aware that currently, if you search for an invalid user name, it will break the app. Don’t worry about this right now; we’ll sort that out in a bit.
We have chained
flatMap
after our first observable, subscribed to the resulting observable, and written some of the received data to the DOM.We have carried out the request using a standard jQuery get, which we have wrapped in RxJS’ helpful
fromPromise
to turn it into an observable.FlatMap
Take a look at the use of `flatMap` in the example. I must admit, when I first looked at a few examples of this kind of thing, it confused the hell out of me. At first glance it looks like we should just be able to call
map
like we did for the click event, after all, we’ve already converted the promise into an observable by calling fromPromise
right?In actual fact, what
fromPromise
returns is an observable stream of promises, not a stream of the objects that the promise would emit when it resolves. flatMap
allows us to flatten all of those promise resolutions into a single observable stream, and when we subscribe to it, we get just the response object that jQuery would originally emitted on then()
.Earlier I mentioned that the app currently breaks if you search for a invalid user name (try searching for some gibberish followed by something perfectly valid). This obviously sucks, and initially, it is not at all obvious why this is happening. To understand why, let’s look at the order of events that takes place as the promise resolves:
1. HTTP request completes, and jQuery rejects the promise (because it’s a 404)
2. The observable created by
fromPromise
throws an error, as this is how it reacts to a rejected promise3. The error goes uncaught, and hence gets thrown again in the parent
flatMap
observable4. The
flatMap
observable will no longer emit because after an observable stream has thrown an error, it is terminated.This has the unintended side effect of making our search button useless every time we get an error response. This isn’t very good, so how can we deal with this problem?
Here’s the app again, but this time with error handling:
<div class="container">
<!-- Search controls -->
<h1>Search Github Users</h1>
<div class="form-group">
<label for="search-box">
Username:
</label>
<input
id="search-box"
type="text"
class="form-control"
/>
</div>
<button
id="search-button"
class="btn btn-primary"
>
Search
</button>
<hr />
<!-- Search result -->
<a
href=""
target="_blank"
id="search-result"
style="display:none;"
>
<h2 id="search-result__login"></h2>
<img
src=""
alt="avatar"
width="150px"
height="150px"
id="search-result__avatar"
/>
</a>
<!-- Error message -->
<div
id="error"
class="well"
style="display:none;"
>
<p id="error__message">
</p>
</div>
</div>
let userClicksSearchButton = Rx.Observable.fromEvent(
$("#search-button"),
'click'
)
.map((event) => {
return $("#search-box").val();
});
userClicksSearchButton
.flatMap((searchTerm) => {
return Rx.Observable.fromPromise(
$.get('https://api.github.com/users/' + searchTerm)
)
.catch((response) => {
renderError(response.statusText);
return Rx.Observable.empty();
});
})
.subscribe((response) => {
renderUser(
response.login,
response.html_url,
response.avatar_url
);
});
function renderUser(login, href, imgSrc) {
$("#search-result").show();
$("#error").hide();
$("#search-result").attr("href", href);
$("#search-result__avatar").attr('src', imgSrc);
$('#search-result__login').text(login);
}
function renderError(message) {
$("#search-result").hide();
$("#error").show();
$("#error__message").text(message);
}
Example 3: Button Search With Error Handling
View the live example in codepen
Now, we catch and handle the error before it travels upstream, and replace the observable with an empty completed one that will get flattened in it’s place. For this we use the handy
Rx.Observable.empty()
.Let’s alter the example a bit. Suppose instead of clicking a button to search, we want the user to be able to type into the box, and have their search be carried out as they type. For this we will need to replace the userClicksSearchButton observable with a userTypesInSearchBox observable like so:
<div class="container">
<!-- Search controls -->
<h1>Search Github Users</h1>
<div class="form-group">
<label for="search-box">
Username:
</label>
<input
id="search-box"
type="text"
class="form-control"
/>
</div>
<hr />
<!-- Search result -->
<a
href=""
target="_blank"
id="search-result"
style="display:none;"
>
<h2 id="search-result__login"></h2>
<img
src=""
alt="avatar"
width="150px"
height="150px"
id="search-result__avatar"
/>
</a>
</div>
let userTypesInSearchBox = Rx.Observable.fromEvent(
$("#search-box"),
'keyup'
)
.map((event) => {
return $("#search-box").val();
});
userTypesInSearchBox
.flatMap((searchTerm) => {
return Rx.Observable.fromPromise(
$.get('https://api.github.com/users/' + searchTerm)
).catch(() => Rx.Observable.empty());
})
.subscribe((response) => {
renderUser(
response.login,
response.html_url,
response.avatar_url
);
});
function renderUser(login, href, imgSrc) {
let searchResult = $("#search-result");
searchResult.show();
searchResult.attr("href", href);
$("#search-result__avatar").attr('src', imgSrc);
$('#search-result__login').text(login);
}
Example 4: Text Search
View the live example in codepen
We now find that because each of our requests are fired in quick succession, we can no longer guarantee that they will complete in the order they were initiated. This could cause our searches to result in a non-matching avatar being displayed underneath. This isn’t great, so to solve this problem, we will use concatMap.
ConcatMap
concatMap
is a lot like flatMap
except it will preserve the order of the source emissions, even if the observable it is flattening emits in a different order. For example, if I search for ‘Elle’, and immediately afterwards for ‘Ellen’, and the ‘Ellen’ request happens to complete first, concatMap
will wait until the ‘Elle’ request has completed, before emitting both results in immediate succession in the order ‘Elle’ ‘Ellen’.Here’s the code, amended to use ‘concatMap’.
<div class="container">
<!-- Search controls -->
<h1>Search Github Users</h1>
<div class="form-group">
<label for="search-box">
Username:
</label>
<input
id="search-box"
type="text"
class="form-control"
/>
</div>
<hr />
<!-- Search result -->
<a
href=""
target="_blank"
id="search-result"
style="display:none;"
>
<h2 id="search-result__login"></h2>
<img
src=""
alt="avatar"
width="150px"
height="150px"
id="search-result__avatar"
/>
</a>
<!-- Error message -->
<div
id="error"
class="well"
style="display:none;"
>
<p id="error__message">
</p>
</div>
</div>
let userTypesInSearchBox = Rx.Observable.fromEvent(
$("#search-box"),
'keyup'
)
.map((event) => {
return $("#search-box").val();
});
userTypesInSearchBox
.concatMap((searchTerm) => {
return Rx.Observable.fromPromise(
$.get('https://api.github.com/users/' + searchTerm)
)
.catch(() => Rx.Observable.empty());
})
.subscribe((response) => {
renderUser(
response.login,
response.html_url,
response.avatar_url
);
});
function renderUser(login, href, imgSrc) {
$("#search-result").show();
$("#search-result").attr("href", href);
$("#search-result__avatar").attr('src', imgSrc);
$('#search-result__login').text(login);
}
Example 5: Text Search Preserving Request Order
View the live example in codepen
Edit: I’ve just learned that we could also use
switchMap
for this. This provides the added advantage of cancelling the underlying redundant http request if necessary.At the moment, our app fires a request every single time the user types a letter into the box. This seems like overkill, given that the user isn’t really interested in seeing a result until they’ve typed at least several letters into the box. Why hammer the server when it add nothing to the user experience? Using RxJS, we can ease this problem with a simple extra function call to ‘debounce’.
<div class="container">
<!-- Search controls -->
<h1>Search Github Users</h1>
<div class="form-group">
<label for="search-box">
Username:
</label>
<input
id="search-box"
type="text"
class="form-control"
/>
</div>
<hr />
<!-- Search result -->
<a
href=""
target="_blank"
id="search-result"
style="display:none;"
>
<h2 id="search-result__login"></h2>
<img
src=""
alt="avatar"
width="150px"
height="150px"
id="search-result__avatar"
/>
</a>
</div>
let userTypesInSearchBox = Rx.Observable.fromEvent(
$("#search-box"),
'keyup'
)
.map((event) => {
return $("#search-box").val();
});
userTypesInSearchBox
.debounce(250)
.concatMap((searchTerm) => {
return Rx.Observable.fromPromise(
$.get('https://api.github.com/users/' + searchTerm)
)
.catch(() => Rx.Observable.empty());
})
.subscribe((result) => {
renderUser(
result.login,
result.html_url,
result.avatar_url,
result.searchTerm
);
});
function renderUser(login, href, imgSrc) {
$("#search-result").show();
$("#search-result").attr("href", href);
$("#search-result__avatar").attr('src', imgSrc);
$('#search-result__login').text(login);
}
Example 6: Text Search Throttled
View the live example in codepen
In this example, it’s worth looking in the network tab of the console window as you type in the box to see that many fewer requests are sent than in the previous example.
Debounce
‘Debounce’ accepts a number parameter that represents the number of milliseconds the observable should wait after the previous emission before emitting again. After the time has elapsed, the observable will emit the last emission from within the time period only, ignoring any others. I’ve found this to be very helpful for live text-based search features.
Note: if you need it to emit the opposite, i.e. the first emission from within the time period, you can use the
throttle
method instead.The above techniques will handle most HTTP request use cases, but there are some other cool things you can do to ease some of the pain that comes from asynchronous request handling:
Take
Sometimes, you’re only interested in the first 1, 2 or n user interactions. For these situations RxJS provides the ‘take’ method. In our search button example, lets say we wanted to only show the first valid result:
<div class="container">
<!-- Search controls -->
<h1>Search Github Users</h1>
<div class="form-group">
<label for="search-box">
Username:
</label>
<input
id="search-box"
type="text"
class="form-control"
/>
</div>
<hr />
<!-- Search result -->
<a
href=""
target="_blank"
id="search-result"
style="display:none;"
>
<h2 id="search-result__login"></h2>
<img
src=""
alt="avatar"
width="150px"
height="150px"
id="search-result__avatar"
/>
</a>
</div>
let userTypesInSearchBox = Rx.Observable.fromEvent(
$("#search-box"),
'keyup'
)
.map((event) => {
return $("#search-box").val();
});
userTypesInSearchBox
.debounce(250)
.concatMap((searchTerm) => {
return Rx.Observable.fromPromise(
$.get('https://api.github.com/users/' + searchTerm)
)
.catch(() => Rx.Observable.empty());
})
.take(1)
.subscribe((response) => {
renderUser(
response.login,
response.html_url,
response.avatar_url
);
});
function renderUser(login, href, imgSrc) {
$("#search-result").show();
$("#search-result").attr("href", href);
$("#search-result__avatar").attr('src', imgSrc);
$('#search-result__login').text(login);
}
Example 7: Text Search With Take
View the live example in codepen
Now, the first search performed will work, but anything subsequently typed into the box will be ignored.
Filter
Sometimes we want to filter out certain observable emissions based on a condition. To demonstrate, let’s use
filter
to make our text search example ignore searches until they are at least 5 characters long:<div class="container">
<!-- Search controls -->
<h1>Search Github Users</h1>
<div class="form-group">
<label for="search-box">
Username:
</label>
<input
id="search-box"
type="text"
class="form-control"
/>
</div>
<hr />
<!-- Search result -->
<a
href=""
target="_blank"
id="search-result"
style="display:none;"
>
<h2 id="search-result__login"></h2>
<img
src=""
alt="avatar"
width="150px"
height="150px"
id="search-result__avatar"
/>
</a>
</div>
let userTypesInSearchBox = Rx.Observable.fromEvent(
$("#search-box"),
'keyup'
)
.map((event) => {
return $("#search-box").val();
});
userTypesInSearchBox
.debounce(250)
.filter((searchTerm) => {
return searchTerm.length >= 5;
})
.concatMap((searchTerm) => {
return Rx.Observable.fromPromise(
$.get('https://api.github.com/users/' + searchTerm)
)
.catch((response) => Rx.Observable.empty());
})
.subscribe((response) => {
renderUser(
response.login,
response.html_url,
response.avatar_url
);
});
function renderUser(login, href, imgSrc) {
$("#search-result").show();
$("#search-result").attr("href", href);
$("#search-result__avatar").attr('src', imgSrc);
$('#search-result__login').text(login);
}
Example 8: Text Search Filtered
View the live example in codepen
Merge
Sometimes, it is useful to merge more than one observable into a single observable. To demonstrate, let’s use
merge
to give our search app two inputs which you can search by typing into either one:<div class="container">
<!-- Search controls -->
<h1>Search Github Users</h1>
<div class="form-group">
<label for="search-box-1">
Username:
</label>
<input
id="search-box-1"
type="text"
class="form-control"
/>
</div>
<div>
<label for="search-box-2">
Username:
</label>
<input
id="search-box-2"
type="text"
class="form-control"
/>
</div>
<hr />
<!-- Search result -->
<a
href=""
target="_blank"
id="search-result"
style="display:none;"
>
<h2 id="search-result__login"></h2>
<img
src=""
alt="avatar"
width="150px"
height="150px"
id="search-result__avatar"
/>
</a>
</div>
let userTypesInSearchBox1 = Rx.Observable.fromEvent(
$("#search-box-1"),
'keyup'
)
.map((event) => {
return $("#search-box-1").val();
});
let userTypesInSearchBox2 = Rx.Observable.fromEvent(
$("#search-box-2"),
'keyup'
)
.map((event) => {
return $("#search-box-2").val();
});
userTypesInSearchBox1
.merge(userTypesInSearchBox2)
.debounce(250)
.concatMap((searchTerm) => {
return Rx.Observable.fromPromise(
$.get('https://api.github.com/users/' + searchTerm)
)
.catch(() => Rx.Observable.empty());
})
.subscribe((response) => {
renderUser(
response.login,
response.html_url,
response.avatar_url
);
});
function renderUser(login, href, imgSrc) {
$("#search-result").show();
$("#search-result").attr("href", href);
$("#search-result__avatar").attr('src', imgSrc);
$('#search-result__login').text(login);
}
Example 9: Text Search With Merge
View the live example in codepen
Keeping data for later
A potential issue with all of the examples above is that when we are in the latter stages of the observable stream, after the request has completed, we’ve lost the reference to the original payload (in this case, our search term text). We can get around this bundling the original payload in with the response payload while we still have access to it via lexical scoping. For example, let’s say we wanted to write the search term to the page again as a part of the
render
function:<div class="container">
<!-- Search controls -->
<h1>Search Github Users</h1>
<div class="form-group">
<label for="search-box">
Username:
</label>
<input
id="search-box"
type="text"
class="form-control"
/>
</div>
<hr />
<!-- Search Term -->
<div>
<span>
<strong>Search Term:</strong>
</span>
<span id="search-term-text"></span>
</div>
<!-- Search result -->
<a
href=""
target="_blank"
id="search-result"
style="display:none;"
>
<h2 id="search-result__login"></h2>
<img
src=""
alt="avatar"
width="150px"
height="150px"
id="search-result__avatar"
/>
</a>
</div>
let userTypesInSearchBox = Rx.Observable.fromEvent(
$("#search-box"),
'keyup'
)
.map((event) => {
return $("#search-box").val();
});
userTypesInSearchBox
.debounce(250)
.concatMap((searchTerm) => {
return Rx.Observable.fromPromise(
$.get('https://api.github.com/users/' + searchTerm)
)
.map((response) => {
return {
response: response,
searchTerm: searchTerm
}
})
.catch(() => Rx.Observable.empty());
})
.subscribe((result) => {
renderUser(
result.response.login,
result.response.html_url,
result.response.avatar_url,
result.searchTerm
);
});
function renderUser(login, href, imgSrc, searchTerm) {
$("#search-result").show();
$("#search-result").attr("href", href);
$("#search-result__avatar").attr('src', imgSrc);
$('#search-result__login').text(login);
$('#search-term-text').text(searchTerm);
}
Example 10: Text Search With Retained Data
View the live example in codepen
Combine latest
One of the best features of RxJS is how easy it is to combine multiple streams into one. To demonstrate, let’s adapt our example to carry out two searches and allow the user to compare the resulting avatars next to each other, but only after both searches have a result:
<div class="container">
<!-- Search controls -->
<h1>Compare Github Users</h1>
<div class="row">
<div class="col-xs-6">
<div class="form-group">
<label for="search-box">
Username 1:
</label>
<input
id="search-box-1"
type="text"
class="form-control"
/>
</div>
</div>
<div class="col-xs-6">
<div class="form-group">
<label for="search-box">
Username 2:
</label>
<input
id="search-box-2"
type="text"
class="form-control"
/>
</div>
</div>
</div>
<hr />
<div class="row">
<div class="col-xs-6">
<!-- Search result 1 -->
<a
href=""
target="_blank"
id="search-result-1"
style="display:none;"
>
<h2 id="search-result-1__login"></h2>
<img
src=""
alt="avatar"
width="150px"
height="150px"
id="search-result-1__avatar"
/>
</a>
</div>
<div class="col-xs-6">
<!-- Search result 2 -->
<a
href=""
target="_blank"
id="search-result-2"
style="display:none;"
>
<h2 id="search-result-2__login"></h2>
<img
src=""
alt="avatar"
width="150px"
height="150px"
id="search-result-2__avatar"
/>
</a>
</div>
</div>
</div>
let userTypesInSearchBox1 = Rx.Observable.fromEvent(
$("#search-box-1"),
'keyup'
)
.map((event) => {
return $("#search-box-1").val();
});
let userTypesInSearchBox2 = Rx.Observable.fromEvent(
$("#search-box-2"),
'keyup'
)
.map((event) => {
return $("#search-box-2").val();
});
let searchResult1 = userTypesInSearchBox1
.debounce(250)
.concatMap((searchTerm) => {
return Rx.Observable.fromPromise(
$.get('https://api.github.com/users/' + searchTerm)
)
.catch((response) => {
return Rx.Observable.empty();
});
});
let searchResult2 = userTypesInSearchBox2
.debounce(250)
.concatMap((searchTerm) => {
return Rx.Observable.fromPromise(
$.get('https://api.github.com/users/' + searchTerm)
)
.catch((response) => {
return Rx.Observable.empty();
});
});
Rx.Observable
.combineLatest(searchResult1, searchResult2)
.subscribe((results) => {
renderUsers(
results[0].login,
results[0].html_url,
results[0].avatar_url,
results[1].login,
results[1].html_url,
results[1].avatar_url
);
});
function renderUsers(
login1,
href1,
imgSrc1,
login2,
href2,
imgSrc2
) {
$("#search-result-1").show();
$("#search-result-1").attr("href", href1);
$("#search-result-1__avatar").attr('src', imgSrc1);
$('#search-result-1__login').text(login1);
$("#search-result-2").show();
$("#search-result-2").attr("href", href2);
$("#search-result-2__avatar").attr('src', imgSrc2);
$('#search-result-2__login').text(login2);
}
Example 11: Text Search With Combine Latest
View the live example in codepen
What next?
There are many ways to get the most out of the power of RxJS to handle HTTP requests, and we’ve touched upon a few of these. However, the RxJS API is a complex beast and we’ve only scratched the surface on the many different ways RxJS methods can do cool stuff with HTTP streams. Once you’ve got your head round a few, it’s well worth checking out the docs to experiment with a few more that might help out in more specific use cases