Understanding Async JavaScript & Observables - Part 1

RXJS Dec 18, 2019



As a frontend developer, you likely have come across or used ReactiveX extensions for async programming. The ReactiveX project provides an API for async programming in different programming languages (the JavaScript implementation is called RxJS).
The key abstraction in all ReactiveX implementations is the Observable. Understanding Observables and their paradigm is fundamental to understanding at async programming.

The focus of this blog will be to understand Observables from its first principles. We will go through its origins, its need in modern JavaScript applications and have a sneak-peek of its elegant async programming style.

Observables

An Array is an iterable with an iterator that can be used to iterate over the collection. The opposite of this is an Observable which provides an observer that can then be used to subscribe to the Observable.

In ReactiveX an observer subscribes to an Observable. Then, that observer reacts to whatever item or sequence of items the Observable emits.

var scrollEventSubscription = Observable.fromEvent(element, 'scroll');
    scrollEventSubscription.subscribe(event => console.log(event));

Observable Origin

First, let us understand the origins of Observable. What are the key concepts based on which Observables were derived and what was the need for them?

The key to understanding Observables:

  • The Iterator and Observer Design patterns (and the connection between the two).
  • Events as Streams

The Iterator and Observer Design patterns

The two design patterns that Observable is based on are the Iterator and Observer Design patterns. We will briefly go through both these patterns:

Iterator Pattern
In this pattern, there is a producer and a consumer. The consumer requests information from the producer one at a time until one of the following happens:

  • No more data available  
  • An error occurs

Here is an example of iterator pattern in JavaScript (Iterators are part of JavaScript language from ES2015 / ES6):

    var doctorCategories = ['General Practitioner', 
                            'Dentist', 
                            'Skin Specialist', 
                            'Neurologist'];

    const iterator = doctorCategories[Symbol.iterator]();
    let iteratorResult = iterator.next();

    while (!iteratorResult.done) {
      console.log(iteratorResult.value);
      iteratorResult = iterator.next();
    }

So, with iterators, we can pull elements out one by one sequentially out of any Collection/Data Structure without knowing its internal representation.

Observer Pattern
This pattern mainly concerns the UI Events i.e how does the model communicate with the view after a state change and vice versa?

For the ones that use JavaScript, this would be a familiar design pattern, the DOM events:

    document.addEventListener('click', function showConfirmDialog(data) {
      confirm();
    });

Here we are registering a callback (which is the Consumer) with the addEventListener method (which is the Producer) on the window object. When the event happens, our callback function is executed with the data i.e the producer pushes data to the consumer. In Push systems, the Producer decides when to send data to Consumer.

The Iterator and the Observer Design patterns essentially do the same thing - progressively send information to the consumer. However, there's one important difference between the two: Unlike the iterator pattern, in the Observer pattern, there is no way to indicate completion and error states to the consumer.

So, we need some well-defined way for the producer to indicate to the consumer these states: next, error and complete. Currently there is no one interface like in iterator for push streams like setInterval, XMLHttpRequest, NodeStreams, Websockets etc. Observables have the semantics which fill this gap.

Events as Streams

When we talk of events in JavaScript, we think of the addEventListener or removeEventListener and not the first class values that you can hold onto like Arrays. But Observables can model anything as a stream i.e a collection whose items arrive over time.
Observable = Collection + time

As events are modelled as a stream in Observables, it gives the ability to compose and transform events to derive more complex events; just like we do the map, filter and reduce operations on an array:

    var topRatedDoctors = doctorCategories =>
      doctorCategories.result.map(category => category.doctors.filter(doctor => doctor.rating === 5)).concatAll();

    topRatedDoctors(doctorCategories).forEach(doctor => console.log(doctor.name));

In an Observable, it looks something like this:

    // Part 1: Building the Stream - top rated doctors in each category
    var topRatedDoctors = fetchDoctorCategories.pipe(
      map(category => getTopDoctors(category)),
      concatAll()
    );

    //Part 2:Subscribing to the above stream, and doing something with it
    topRatedDoctors.subscribe(topDoctor => console.log(topDoctor.name));

If you notice, the structure of the above code is almost the same as the array example.
It has two parts, the first being building the stream we want and the second one where we subscribe to the stream and do something with it. Observable has tools to convert any Event to an Observable via the RxJS utility methods/operators like fromEvent

Adapt Push APIs to Observables

Why Observables?

In almost every UI interaction, we can see the following pattern: we listen for an event --> the user does something --> we might make an asynchronous call to the server --> maybe we follow it up with an animation.

So, we can say the following are the three most common asynchronous actions in UI:

  • Events
  • Async Server Requests
  • Animations

Observables are capable of modelling all of the above actions. Now, if we model all three of these actions as an Observable in our UI, we can then compose them together seamlessly (with very little code using  RxJS). This is one of the main reasons why Observables are powerful.

Auto-complete example:
One of the most frequently seen features on any website is the auto-complete search box. We may think that the auto-complete feature is easy considering that it just has an input box and as we type, we make calls to the server and show the results.

But, as it turns out, it is not that easy because of:

  1. Race conditions - Let's say the user starts typing with 'a' as the first character. We immediately send a request to the server with the keyword as 'a'. Then, the user types 'b' and this time we will send the request with the keyword as 'ab'.
    Now, there could be a race condition here - there is no guarantee that the first request will be completed before the second i.e we might get results for 'ab' before the ones for 'a' leading us to deliver older results instead of new data.
  2. Performance - Let's say the user types 'abcdefg'. Now, since we are making network requests on each key press; in the above case, we would end up making 7 network requests.
  3. Handling retries on error

Now, below is the sample code for auto-complete box written using Observables

// sample code for auto-complete box, using RxJS Library
var searchResults = keyPresses.
                        debounce(250).
                        map(key => 
                           fetch("/searchResults?q=", input.value)).
                           retry(3)
                        ).
                        switchLatest();
                           
 searchResults.subscribe(
                   result => updateSearchResult(result),
                   error => showMessage("Internal Server Error")
                   );
                        

That is surprisingly less code for the work that is done. It is possible because of the Observables way of approaching async programming in Javascript.

In PART 2, we will go through how Observable converts Events into streams ( fromEvent API ).  Also, we will look at how streams are composed and transformed using some of the most commonly used RxJS operators like map, filter, concatAll with marble diagrams.


We are always looking out to hire for all roles in our tech team. If challenging problems that drive big impact enthral you, do reach out to us at careers.india@halodoc.com


About Halodoc

Halodoc is the number 1 all around Healthcare application in Indonesia. Our mission is to simplify and bring quality healthcare across Indonesia, from Sabang to Merauke.
We connect 20,000+ doctors with patients in need through our teleconsultation service, we partner with 1500+ pharmacies in 50 cities to bring medicine to your doorstep, we partner with Indonesia's largest lab provider to provide lab home services, and to top it off we have recently launched a premium appointment service that partners with 500+ hospitals that allows patients to book a doctor appointment inside our application.
We are extremely fortunate to be trusted by our investors, such as the Bill & Melinda Gates foundation, Singtel, UOB Ventures, Allianz, Gojek and many more. We recently closed our Series B round and In total have raised USD$100million for our mission.
Our team work tirelessly to make sure that we create the best healthcare solution personalized for all of our patient's needs, and are continuously on a path to simplify healthcare for Indonesia.