Live search with RxJS- the devil is in the details

Wojciech Trawiński
Angular In Depth
Published in
8 min readAug 1, 2018

AngularInDepth is moving away from Medium. More recent articles are hosted on the new platform inDepth.dev. Thanks for being part of indepth movement!

Although I’ve been using the RxJS library on a day-to-day basis for quite a long time, I still remember when I first got astonished by the library features. It was some time ago when I had a recruitment exercise to do. It was possible to get extra points for creating a live search instead of performing the search action after the form submission. That time I was keen on React, therefore I did the task using this library and it was quite challenging to accomplish the goal. It was necessary to use a third-party live search component in order to debounce the user’s input. Whereas, cancellation of a pending request required using the Axios library for making HTTP requests. When I got familiar with Angular I couldn’t believe that this live search exercise could be accomplished so easily with the aid of the RxJS library, namely that it took only five operators to make it work!

Although the live search example is one of the most popular RxJS benchmarks, there are several aspects worth paying attention to. I will cover them in this article.

Getting notified

Let’s start with a short introduction of the live search example. I have a five-element cars array with three Ferrari and two Porsche cars. I make use of a getCars function which returns an observable of cars which match the current query parameter. To simulate an HTTP call, a value emitted by the observable is delayed. Two helper functions, namely getCarFullName and isCarMatching, return a car’s full name and check if the given car matches a provided query, respectively.

The live example can be found here: https://jsfiddle.net/a52b3zem/3/

The main character of this example is a carSearch$ observable.

Let’s explore the stream step by step:

  • fromEvent is an operator which creates an observable that emits a value when an event specified as the second parameter is fired with a target of the event being the first provided argument (here I’m interesed in every input event fired on the input element with id equal to carSearch),
  • map operator projects an emitted value (input event) to the input’s value, namely the provided query string,
  • filter operator prevents further processing for an empty query string,
  • debounceTime operator stops further processing until a specified period of time has elapsed since the last user’s input. The purpose of the operator is to prevent making an http call for each key stroke,
  • distinctUntilChanged operator stops further processing if a current value is the same as a previous one,
  • tap operator is used here only for debugging purpose, since it allows to track if an emitted value has passed all previous operators,
  • switchMap operator allows to create a new observable for each value emitted by a source observable. In the above example, for each query string which reaches the switchMap operator the getCars function is called which returns an observable (a stream with a single value, namely cars array matching a given query string).

Finally, I subscribe to the carsSearch$ stream and pass a logging function onCarsLoadSuccess which simply console.log an array returned from a call to the getCars function.

The example is working really well, so let’s inspect what can go wrong.

Order matters

In the final code, there will be five operators applied in the pipe method. If you don’t reuse your previous code you may make a mistake, namely provide the operators in a wrong order. Especially one mismatch is quite popular, namely swapping debounceTime and distinctUntilChanged operators.

If you make the above-mentioned mistake (lines 54 and 55), you won’t make us of distinctUntilChanged operator at all, since each value will reach the operator as debounceTime operator comes after the distinctUntilChanged one.

Live example can be found here: https://jsfiddle.net/3oazjxeu/1/

Let’s examine a simple scenario:

  • a previous search was done for a query string equal to ‘porsche’,
  • a user deletes some trailing characters (a query string equals to ‘pors’),
  • a user immediately types the remaining letters (a query string equals to ‘porsche’),
  • a required time period elapses (1 second in the example),
  • the getCars function is invoked with the same query string as the last time.

It’s definitely not what you intended to. There is no point in making another http call with the same query string as the last time. However, it’s all your fault. You didn’t provide the operators in the right order.

Need for switch

In the RxJS library there is a group of four operators which you can use to map a value from an outer observable to an inner stream. The following operators can be used:

  • switchMap,
  • exhaustMap,
  • concatMap,
  • mergeMap.

You may be tempted to use a different operator than the one used in the benchmark code (switchMap), but you will get into troubles soon. Let’s see what will go wrong if you use other operator than switchMap.

switchMap

The most important switchMap operator’s feature is that the resulting stream is fed with values only from the last inner observable created on the basis of a value emitted by an outer observable.

Let’s examine a simple scenario:

  • a user types ‘porsche’ into the input,
  • switchMap operator is reached and a call to getCars function is made with a given query string,
  • a mocked request is pending,
  • if a user change the input’s value and the switchMap operator is reached while the previous request is still pending, the former request will be aborted and a new one will be processed. The previous subscription will be unsubscribed and the new one will be made,
  • the value emitted by the resulting observable will be in sync with the current input’s value.

exhaustMap

The operator can be used if you want to discard all the attempts to make a new request while there is a pending one.

Let’s examine a simple scenario:

  • a user types ‘porsche’ into the input,
  • exhaustMap operator is reached and a call to getCars function is made with a given query string,
  • a mocked request is pending,
  • if a user change the input’s value and the exhaustMap operator is reached while the previous request is still pending, no new request will be made,
  • the value emitted by the resulting observable won’t be in sync with the current input’s value.

The main drawback of using exhaustMap operator for the live search case is the lack of synchronization between the emitted value and the input’s value.

Live example can be found here: https://jsfiddle.net/2c3mud1p/1/

However, the operator is the best choice for a login button or refresh button click scenario.

concatMap

The concatMap operator takes into account all created inner observables. However, it keeps a streams queue and subscribes to the next stream only when the previous one has completed.

Let’s examine a simple scenario:

  • a user types ‘porsche’ into the input,
  • concatMap operator is reached and a call to getCars function is made with a given query string,
  • a mocked request is pending,
  • if a user change the input’s value (let’s assume that ‘ferrari’ is typed) and the concatMap operator is reached while the previous request is still pending, an observable returned from the call to getCars function will be added to a streams internal queue,
  • when the currently active stream has completed, the next observable from the queue will be picked and subscribed to,
  • the resulting observable will emit the result for ‘porsche’ query followed by the result for ‘ferrari’ query.

Live example can be found here: https://jsfiddle.net/fmgdbwLr/1/

The important things to note are:

  • all inner observables will be taken into account (subscribed to),
  • the resulting stream values will be in an expected order,
  • no parallel request will be done, so the time to get all the results may be quite long.

mergeMap

The mergeMap operator takes into account all created inner observables. The difference between the mergeMap and concatMap operators is that the former one subscribes immediately to all inner observables (unless you provide a limit on maximum active subscriptions), whereas the latter one has only one active subscription at a time.

Let’s examine a simple scenario:

  • a user types ‘porsche’ into the input,
  • mergeMap operator is reached and a call to getCars function is made with a given query string,
  • a mocked request is pending,
  • if a user change the input’s value (let’s assume that ‘ferrari’ is typed)and the mergeMap operator is reached while the previous request is still pending, an observable returned from the call to getCars function will be subscribed to immediately. Therefore, there will be two active subscriptions,
  • when any of the inner streams emits a value it will be pushed to the resulting stream, so the results may be out of order,
  • the resulting observable will emit the result for both ‘porsche’ and ‘ferrari’ queries. However, you may end up with the Ferrari cars array received before the Porsche cars one.

Live example can be found here: https://jsfiddle.net/nhacdj2v/2/

The important things to note are:

  • all inner observables will be taken into account (subscribed to),
  • the resulting stream values may be out of order,
  • parallel request will be done, so the time to get all the results will be shorter than using the concatMap operator.

Summary

Based on the above analysis, the best choice for the live search exercise is definitely the switchMap operator. However, the remaining operators are useful in different scenarios when the hero operator of today’s article would be a bad choice.

Unique events

The last gotcha I want to tell you about is retrieving an input’s value too late in the sequence of operators.

Live example can be found here: https://jsfiddle.net/hbcuxpy0/2/

Take a look at lines 54 and 55. I delay retrieving the input’s value from the provided event and as a result distinctUntilChanged operator won’t work as expected. Instead of comapring a previous and current query strings, it will compare input events by reference. As a result, each query string hidden inside an event object will be treated as unique and a new request will be made even if a query string will be the same as the previous value.

Conclusions

I hope you enjoyed the article and got familiar with the common gotchas you may come across while implementing a live search with the aid of the RxJS library.

I believe the best idea is to keep reusable logic such as the live search example in a custom operator and simply import it when neeeded.

Feel free to ask any questions.

--

--