Adding Debounce to the Typeahead

One of the ways to determine the quality of code is to see how resilient the code is to change.

Let’s see how the vanilla snippet fares when adding debouncing:

 let​ latestQuery;
 searchBar.addEventListener(​'keyup'​, debounce(event => {
 let​ searchVal = latestQuery = event.target.value;
  fetch(endpoint + searchVal)
  .then(results => {
 if​ (searchVal === latestQuery) {
  updatePage(results);
  }
  });
 }));

Not much of a change, though it’s easy to miss the fact that the event function is debounced, which may lead to confusion if someone inherits the project. One could extract the inner function into a separate variable, adding more code but enhancing clarity. On the other hand, how does the Rx version do?

 fromEvent(searchBar, ​'keyup'​)
 .pipe(
  pluck(​'target'​, ​'value'​),
  debounceTime(333),
  switchMap(query => ajax(endpoint + searchVal))
 )
 .subscribe(results => updatePage(results));

Only one line is added, and where the debounce fits is clear to everyone who reads the code. Specifically, this is the debounceTime operator, which works along the lines of the debounce function written in the previous snippet—it waits until there’s a 333 ms gap between events and then emits the most recent event, ignoring the rest. If another developer wants to change where the debounce happens or the length of the debounce, it’s obvious how that change is accomplished.

Code quality is often a subjective metric, but you can already see how organized code becomes with RxJS. Everything is written in the order it’s executed. Variables are declared close to where they’re used (often on the same line), guarding against a whole category of scoping bugs. Each unit of functionality is encapsulated in its own function, without cross-cutting concerns. For the rest of this example, we’ll drop the vanilla JavaScript and just use RxJS. This is, after all, a book about RxJS.

Skipping Irrelevant Requests

Now that the typeahead has debounceTime plugged in, far fewer requests are sent. That said, a lot of requests are still being sent, so there’s work yet to do. You have two more tricks up your sleeve to cut down on these superfluous requests. The first is filter (you’ll recall from Chapter 2, Manipulating Streams), which you can use to remove items that won’t provide useful results. Requests of three or fewer characters aren’t likely to provide relevant information (a list of all the StackOverflow questions that include the letter a isn’t terribly helpful), so filter allows searches only where the query has more than three characters:

 fromEvent(searchBar, ​'keyup'​)
 .pipe(
  pluck(​'target'​, ​'value'​),
  filter(query => query.length > 3),
  debounceTime(333),
  switchMap(query => ajax(endpoint + searchVal))
 )
 .subscribe(results => updatePage(results));

So far, so good. This code only makes a request when the user stops typing, and there’s a detailed enough query to be useful. There’s one last optimization to make: keyup will fire on any keystroke, not just one that modifies the query (such as the left and right arrow keys). In this case, making a request with an identical query isn’t useful, so you want to dispose of any identical events until there’s a new query. Unlike the generic filter operator that looks at only one value at a time, this is a temporal filter. Some state handling is involved, since this new filter needs to compare each value to a previously-stored one. Instead of dealing with the messy state handling ourselves, Rx provides the distinctUntilChanged operator. distinctUntilChanged works just how you want it to—it keeps track of the last value to be passed along, and only passes on a new value when it is different from the previous value. You can add this in with a single line and head out for an early lunch.

 fromEvent(searchBar, ​'keyup'​)
 .pipe(
  pluck(​'target'​, ​'value'​),
  filter(query => query.length > 3),
  distinctUntilChanged(),
  debounceTime(333),
  switchMap(query => ajax(endpoint + searchVal))
 )
 .subscribe(results => updatePage(results));

Handling Response Data

Right now, a single function (updatePage) is handling all the results. There’s also no error handling. Quick, add an error handler using the techniques you learned in Chapter 3, Managing Asynchronous Events:

 fromEvent(searchBar, ​'keyup'​)
 .pipe(
  pluck(​'target'​, ​'value'​),
  filter(query => query.length > 3),
  distinctUntilChanged(),
  debounceTime(333),
  switchMap(query => ajax(endpoint + searchVal))
 )
 .subscribe(
  results => updatePage(results),
  err => handleErr(err)
 );

This error handler handles the error gracefully and unsubscribes from the stream. When your observable enters the errored state, it no longer detects keystrokes, and the typeahead stops working. We need some way to handle errors without entering the error state. The catchError operator does just that.

Using catchError

The catchError operator is simple on the surface—it triggers whenever an error is thrown, but it provides plenty of options for how you handle the next steps. catchError takes two parameters: the error that was thrown and the current observable that’s being run. If all we cared about in an error state was that an error was thrown, we could write the catchError operator like this:

 catchError(err => {
 throw​ err;
 })

This catchError function acts as if it had never been included in the first place. For the use of catchError to make sense, one common use case is to throw a new, more descriptive error:

 catchError(err => {
 throw​ ​'Trouble getting predictions from the server'​;
 })

This still results in the observable entering the errored state, but the error is clear. Now, what if we want to continue on instead of entering the errored state? We need to tap into the second parameter passed to catchError—the observable itself. This is tricky to conceptualize, so let’s start with the code:

 catchError((err, caught$) => {
 return​ caught$;
 })

If the catchError operator doesn’t throw a new error, Rx takes a look at what it has returned. Rx looks for anything that can be easily turned into an observable; an array, a promise, or another observable are all valid options. Rx then converts the return value into an observable (if needed), and now the rest of the observable chain can subscribe to the new, returned observable. If all catchError does is return the original observable, the rest of the chain continues unabated.

However, we don’t want to completely ignore errors—it’d be nice if we could note the error somehow without completely breaking the typeahead. In other words, we want to return a new observable that contains both an object with error information as well as the original observable. This is the perfect case for the merge operator you learned about in Chapter 3, Managing Asynchronous Events.

 catchError((err, caught$) => {
 return​ merge(​of​({err}), caught$);
 })

In the typeahead, we add catchError right after the switchMap, so it can catch any AJAX errors. We want the typeahead to keep working even when things go wrong, so we borrow the merge pattern.

 fromEvent(searchBar, ​'keyup'​)
 .pipe(
  pluck(​'target'​, ​'value'​),
  filter(query => query.length > 3),
  distinctUntilChanged(),
  debounceTime(333),
  switchMap(query => ajax(endpoint + searchVal)),
  catchError((err, caught$) =>
  merge(​of​({err}), caught$)
  )
 )
 .subscribe(​function​ updatePageOrErr(results) {
 if​ (results.err) {
  displayErr(results.err);
  } ​else​ {
  displayResults(results.data);
  }
 });

Notice that the function passed into subscribe has also changed. updatePageOrErr is smart enough to check whether the err property exists on results and display a handy error message instead of the results. Semantically speaking, this is a bit confusing—the code now treats an error like any other value. At this point, it’s better to think of an event as an update, rather than always containing new data for the typeahead. However, this allows our UI to be informative (errors are happening) without dying on the first error.

One finishing touch—let’s show off a bit and add a loading spinner. We know something’s actually changed when a value hits the switchMap, so just before the switchMap, add a tap operator that will display the loading spinner. Another tap just after the catch (or when the request has completed) will hide the spinner. These tap operations let us isolate side effects from the main business logic:

 fromEvent<any>(searchBar, ​'keyup'​)
 .pipe(
  map(event => event.target.value),
  filter(query => query.length > 3),
  distinctUntilChanged(),
  debounceTime(333),
  tap(() => loadingEl.style.display = ​'block'​),
  switchMap(query => ajax(endpoint + query)),
  catchError((err, caught$) =>
  merge(​of​({ err }), caught$)
  ),
  tap(() => loadingEl.style.display = ​'none'​)
 )
 .subscribe(​function​ updatePageOrErr(results: any) {
 if​ (results.err) {
  alert(results.err);
  } ​else​ {
  displayResults(results.response);
  }
  },
  err => alert(err.message)
 );
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.216.255.250