In the Days of Olde, when magic still roamed the land, a High Programmer insulted a Network, and ever since then, the networks have had it out for us programmers. A typeahead race condition bug typically manifests itself like so:
This could happen for many reasons—an ISP could have directed the abc query through a less-congested router, abc could have had fewer possible answers, resulting in a faster query, or the Network remembered that grudge from a long time ago. Regardless of the reason, our user now has the wrong results in front of them. How can you prevent this terrible tragedy?
Back in the days of VanillaJS, a solution might have started off based on an event listener:
| let latestQuery; |
| searchBar.addEventListener('keyup', event => { |
| let searchVal = latestQuery = event.target.value; |
| fetch(endpoint + searchVal) |
| .then(results => { |
| if (searchVal === latestQuery) { |
| updatePage(results); |
| } |
| }); |
| }); |
Technically, this works though the exterior variable latestQuery might lead to some raised eyebrows in a code review. Look at this observable solution:
| fromEvent(searchBar, 'keyup') |
| .pipe( |
| pluck('target', 'value'), |
| switchMap(query => ajax(endpoint + searchVal)) |
| ) |
| .subscribe(results => updatePage(results)); |
As usual, a new operator has snuck in for you to learn. This time it’s switchMap—an operator that’s been stealing notes from mergeMap. switchMap works the same way as mergeMap: for every item, it runs the inner observable, waiting for it to complete before sending the results downstream. There’s one big exception: if a new value arrives before the inner observable initiated by the previous value completes, switchMap unsubscribes from the observable request (therefore cancelling it) and fires off a new one. This means that you can implement custom unsubscribe logic for your own observables (like the ones you built in Chapter 1, Creating Observables).
In the switchMap example above, abc would be passed to switchMap before the query for ab is finished, and therefore, ab’s result would be thrown away with nary a care. One way to think about this is that switchMap switches to the new request. The Rx version of the typeahead has each step wrapped up in its own functional package, leading to much more organized code. Let’s look at what happens now when the network requests get mixed up:
That’s a lot going on behind the scenes! Thankfully, the RxJS library handles all of these details.
Both the addEventListener and fromEvent snippets are missing part of the requirements—they don’t wait for the user to stop typing before making a request, leading to a lot of unneeded requests. This is a great way to make the backend engineers angry—let’s avoid that. Instead, how about you implement a debounce function?
18.119.163.238