Video lecture of this post found here — https://www.youtube.com/watch?v=gIMMkX4VZ54&t=812s

An autocomplete search for Github usernames. Github repo — https://bit.ly/2nl4Cmr

Autocomplete search boxes are nearly ubiquitous in web applications. They speed up human computer interactions by providing users nearly instantaneous feedback on their query. They decrease frustration by helping users discover available, relevant content faster, thus improving their overall experience.

To build an autocomplete component, a naive solution would be to make a new GET request for matching results every time the input changes. This poses some technical issues.

The average typing speed is 38–40 wpm (words per minutes), and many individuals can type well above 100wpm! If we sent a query to our API for every input event from our search field, we could easily overload the server.

The pancakes are server requests. There are too many for the bunny (server) to process.

Additionally, most of those query requests are redundant. Let’s say I’m a shopper on an e-commerce site and I’m looking for some mittens. By the time I write M-I-T-T in the search field, I’m no longer interested in the results for M or M-I or M-I-T. Even worse, if I make a typo and write M-I-T-T-A and then change the query back to M-I-T-T, then I have made two unnecessary requests for results I already had.

So. Many. Requests.

Even if we weren’t concerned with overloading our server, there would still be an unavoidable technical hurdle. There is no guarantee that we would receive results from our server in the order we sent out our requests!

Imagine that I wrote M-I-T. There are likely more results in my database for M -I than for M-I-T, which might make the M-I query slower than the M-I-T query. Consequently, I might get the results for M-I-T, and afterward, the out-of-date results for M-I amble in. The autocomplete suggestions are now out of sync with the search query!

Out of sync requests are disorienting.

Enter Observables

An Observer observes a stream of events received over time. The stream is called an observable. We can then configure our observable to emit a stream of events based on the incoming stream. So rather than doing a GET request for every input event, we configure the stream to respond to only certain events.

Milkshakes and pancakes stream over time. Ralph observes that stream and passes milkshakes to the cat and pancakes to the bunny.

There are a few libraries out there for observables, but RxJS is the most popular. Because Angular relies on RxJS, Google contributes heavily to the RxJS open source project. Also, ReactiveX (the Rx of RxJS) has libraries for several languages, not just JavaScript. In this walkthrough, I’ll be using version 6 which was released in April, 2018. I will have a future post that goes into the differences between v5 and v6.

Boilerplate — Github Repo: https://bit.ly/2nl4Cmr

To demonstrate the power of observables, we will build a simple autocomplete search for Github usernames. It will have a search field and a place to append the results.

Here’s some starter HTML code with the RxJS v6 script tag:

<body>

<input type="text" name="search" id="search"/>



<ul id="results">

</ul> <script src="https://unpkg.com/rxjs@6.2.2/bundles/rxjs.umd.min.js"></script>

<script src="main.js"></script>

</body>

And here’s the boilerplate code for our main.js file:

const {

fromEvent,

from,

} = rxjs;

const {

map,

filter,

distinctUntilChanged,

debounceTime,

switchMap,

} = rxjs.operators; let searchBox = document.getElementById('search');

let results = document.getElementById('results'); let searchGithub = (query) =>

fetch(`https://api.github.com/search/users?q=${query}`)

.then(data => data.json());

Operators

This is the start of our observer:

let input$ = fromEvent(searchBox, 'input')

.pipe();

Notice that the variable representing our observer, input$, ends in a “$”: this is a convention for observables.

fromEvent() takes in a DOM element as its first argument and an event type as its second argument. Our code uses fromEvent() to create a data stream of all the search box’s input events i.e. an observable.

.pipe() is a method of observables that allows us to manipulate the outgoing data stream with operators. We will feed operators as arguments to .pipe().

.map

This .map takes in an event (1, 2, 3) and emits a corresponding event that is the event multiplied by 10 (10, 20, 30).

The .map() operator applies a function to each event from the source observable, and returns a stream of transformed events.

For our autocomplete, we observe each input event from the search field, but we are only interested in one value in that event object: event.target.value.

Instead of observing every input event and then emitting the whole event object, with .map we now observe every input event object and emit only the .target.value property of those event objects.

let input$ = fromEvent(searchBox, 'input')

.pipe(

map(e => e.target.value)

);

.filter

This .filter only emits the event that are not even.

Suppose that we don’t want to make a search request to Github for username matches unless the user has inputted at least two letters. If the user typed just one letter, the results would be meaningless because of the number of results.

At this point, our .map() is only passing strings to .filter(). So for our autocomplete, we will only emit the queries that have a length of at least 2.

We also want empty queries to be emitted. We will make sure that the GET request for an empty query returns no results in a later operator.

let input$ = fromEvent(searchBox, 'input')

.pipe(

map(e => e.target.value),

filter(query => query.length >= 2 || query.length === 0)

);

.debounceTime

Every time an event is passed to .debounceTime, it waits 10ms for other events. If a new event happens in that time, it restarts the clock. Then , when 10ms has passed, it emits the most recent event.

So far, we haven’t used an operator that requires an observable. If we had a regular input event listener on our searchBox element, we could have done an if statement to send our request only if the length of event.target.value was at least 2.

.debounceTime() is when observing events over time becomes relevant. The only argument .debounceTime() needs is a number (in milliseconds).

Whenever .debounceTime() receives an event, it waits a designated amount of time to see if another event comes down the pipe. If it does, it restarts its timer. When enough time has passed without another event streaming in, it emits the latest event.

Now when the user types, we only send a request to our server when they stop typing.

let input$ = fromEvent(searchBox, 'input')

.pipe(

map(e => e.target.value),

debounceTime(250),

filter(query => query.length >= 2 || query.length === 0),

distinctUntilChanged(),

);

.distinctUntilChanged

The second and third event put into .distinctUntilChanged both had a value of 2. Therefore, since the value of the third marble didn’t change from the second marble, it did not get emitted.

.distinctUntilChanged() is one of the simpler operators. Take the example I had in the introduction: a user searches for “mittens”, but makes a mistake. They type M-I-T-T-A and then delete back to M-I-T-T. Let’s pretend we already had the results for M-I-T-T. Since the query didn’t change from the last emitted data, .distinctUntilChanged() doesn’t let the event through.

let input$ = fromEvent(searchBox, 'input')

.pipe(

map(e => e.target.value),

debounceTime(250),

filter(query => query.length >= 2 || query.length === 0),

distinctUntilChanged()

);

.switchMap

Marble “A” from the first line emits marble “1”, “2”, and “3” listed on the second line. “A3” does not get outputted on the thirds line because it has switched to observable “B” and only its emitted events.

The final, and most complicated, piece of our .pipe() configuration is .switchMap(). It will make sure we only emit the results from the most recently placed GET request.

.switchMap() takes in a callback that returns an observable. We can make an observable from a promise with from() which emits the results of the promise when it resolves.

.switchMap(value => from(searchGithub(value)))

.switchMap() will emit the events that resolve from the inner observable i.e. it will return the resolved promise .

If a new query value streams in, however, .switchMap() abandons the old observable and switches to the new observable. In our case, this is a new promise made from the updated query. Therefore, our observable will only emit values from the newest query.

As mentioned in the section on .filter(), if the emitted string is empty, we want the results array to clear. So instead of just returning the observable directly, we’ll add a ternary statement to check if the value that streamed in is empty. If the value is empty, instead of sending a request we will just resolve our observable to an empty array.

The Github request for usernames returns an object where the information we want is listed in .items, so that object is represented in the ternary statement below.

let input$ = fromEvent(searchBox, 'input')

.pipe(

map(e => e.target.value),

debounceTime(250),

filter(query => query.length >= 2 || query.length === 0),

distinctUntilChanged(),

switchMap(value => value ?

from(searchGithub(value)) : from(Promise.resolve({items: []}))

)

);

Subscribe

We’ve observed a stream going in and now we need to listen to the stream of outgoing events. We do so by using the method .subscribe() on our observable.

For every event that enters our observable and survives the gauntlet of our operators, we need to give the resulting emitted events a callback. To start, every time we receive an event, we will empty our results <ul> so we only display the newest results.

input$.subscribe(data => {

while (results.firstChild) {

results.removeChild(results.firstChild);

}

});

As you can see above, we are representing the emitted event as data, and we will map through the array of results. In the case of this Github returned request, the results we are interested are data.items. Then, we will create an <li> for each .login of each result in the array.

input$.subscribe(data => {

while (results.firstChild) {

results.removeChild(results.firstChild);

}

data.items.map(user => {

let newResult = document.createElement('li');

newResult.textContent = user.login;

results.appendChild(newResult);

});

});

Put that all together and we have an autocomplete search!

The final result.

Conclusion

RxJS has a TON of different operators that can modify a data stream, and our autocomplete just uses a sampling of them. You can see all the available operators in their documentation here: https://rxjs-dev.firebaseapp.com/api

I also highly recommend http://rxmarbles.com/ for visual representations of how operators effect data streams. Each diagram has an interactive slider to see how events are affected and it is super awesome.

Not only does RxJS have a ton of operators, but also a bunch of different built-in observers. I used the inbuilt observers fromEvent() and from() for simplicity, but there all kinds of preconfigured observables included in the RxJS library. Additionally, you can construct an observable from scratch that will have the ability to observe any kind of events you can think of!

I hope to return to some of these more detailed configurations in the future, but in the mean time, happy autocompleting!

You did it!

In the next article we will explore how to use observables in the React framework and map the emitted stream to props.

P.S. .debounceTime() should be the first operator in our pipe, but I wrote it third for the flow of explanation. Both orderings work, but putting .debounceTime() first will stop more events from being passed further down the pipe.