Building a Search Box with RxJS Observables + the Fetch API

Just looking for code? jsfiddle here.

Solving complicated asynchronous problems in JavaScript can be A Hard Thing. The moment you stray beyond the most basic callbacks, it’s easy to fall into tangly, brittle code that’s hard to wrap your head around. This state is often fondly referred to by JS devs as “callback hell”.

There are tons of strategies out there to handle async stuff cleanly, and over the past couple of years, I’ve grown comfortable with Promises as my go-to. But recently I’ve been dipping my toes into more “Reactive” strategies with RxJS, specifically its Observables, and with that mental shift has come a really powerful and expressive tool I’m glad I added to my belt.

In this post we’ll build a simple example using RxJS Observables and the Fetch API: a search box. As the user types, it will retrieve and display relevant search results from the network.

First, a quick note on the Fetch API

The Fetch API is a new way to make network requests from the browser. It uses Promises. It’s supported in all major browsers but still in beta, so you’ll want polyfills if you want to use it IRL. If you’re not familiar with Fetch, here’s a basic example:

Notice there are two then()s. That’s because we’re dealing with two “layers” of Promises here. The first one makes the request, and the second one parses the response body (in our case, as JSON). This is a simplified explanation. For more reading on Fetch, look here and here and here.

Ok, let’s build a search box!

A naïve approach

A simple (but buggy) approach might look something like this. Every time the user types a character in the search input, we retrieve new search results. On then(), we add the results to the DOM for display.

Simple enough, yeah? …can you see the problem yet?

The network is reliable, probably

Network requests are unpredictable; you can’t control how long they take to return. As the user types, the requests (for “f”, “fo”, “foo”, etc.) are made in the correct order, but the order they return is based on network timing. Whichever request takes longest to return is the one that gets displayed. Womp womp.

You could debounce or throttle the search, and that would help, but that solution would still depend on somewhat consistent network request timing. Can we find a watertight way to ensure that search results for only the latest keystroke will appear, while avoiding callback hell?

RxJS Observables: cool & composable

Observables are streams of data that “emit” items asynchronously, and anyone who cares can “subscribe” to them to hear about these pieces of data and react to them however they want.

I actually like to use the word “stream” in place of “Observable” when thinking about these problems. This might be an oversimplification, but for newbies like me, I think it’s a fine way to conceptualize it. RxJS Observables behave a lot like any other iterable; they can be filtered and mapped and composed in all kinds of ways.

This search box is a perfect candidate for the Observable pattern. There’s a stream of events (keystrokes) we care about and want to respond to in a specific way. So let’s Observable-ify this thing in three steps.

1. Create an observable stream

We start by creating an Observable from the keyup events on the search input. This is a stream that will emit all of these keyup events.

To do this, we use fromEvent(), which is just one of many ways to transform an existing stream-like object into an Observable. All sorts of other stuff can be turned into Observables, too, like arrays/iterablespromises, and callbacks.

2. Manipulate the stream

Then we add some operators to manipulate the stream. Our main goals are to:

  1. filter out the emitted items we don’t care about
  2. transform them into network requests and responses

Like I mentioned, it’s conceptually a lot like manipulating an array: filter it, map it, reduce it. RxJS provides a lot of useful ways to do this.

map(), debounce(), and distinctUntilChanged() are fairly easy to understand, but I want to go into more detail about the other two operators.

  • flatMap() “flattens” or combines items from many streams into one stream. You’ll see in the RxJS docs that a “stream” in this context can be an Observable, Promise, or iterable. After all, a Promise is just a stream with one item (the resolve value).
  • flatMapLatest() does the same thing, but with a twist: it filters out everything but the latest emitted item. So the moment it sees a new item (keyup event), it cancels any previous ones so those Promises won’t even get passed along. Cancellation is a very nice feature of Observables and is exactly what we need to avoid that race condition in the naïve solution.

flatMap is probably the trickiest concept in this whole post, so let’s review the process of how a search term gets mapped to search results.

  1. For each search term, create a Promise that makes a network request for the search results.
  2. Combine the responses from each of those Promises into a single stream.
  3. For each response in the stream, create another Promise that parses the response body as JSON. (remember how Fetch has two “layers” of Promises?)
  4. Combine those responses (the parsed bodies) into a single stream.

So, in just 6 lines of code, we’re fine-tuning our event handling in some really complex ways. See what I mean about expressive?

3. Subscribe & react to the stream

Finally, we subscribe to searchStream, which will emit the search results we’re interested in displaying.

subscribe() takes a function that handles the emitted items. It also takes other functions for error handling and completion, but I’m leaving those out here for simplicity’s sake.

All together now

See it in action in this jsfiddle.

And… that’s it!

Putting together this small example has helped me better understand how to use RxJS Observables and how cool they are, and I’ve since found all kinds of ways to use them to my advantage.

If you’re new to RxJS like me and want to learn more, I like this and this as a starting point.

P.S…

Check out this jsfiddle to confirm that flatMapLatest() solves the race condition in the naïve example above. It’s artificially slowing down Promise resolves to force the race condition. If you open up the console and watch the logs as you type, you’ll see that the correct response is displayed even though requests made earlier are returned later. That’s flatMapLatest() in action. You can guess what happens if you change flatMapLatest() to plain old flatMap().

Leave a Reply

Your email address will not be published. Required fields are marked *