• Home
  • Architecture
  • Angular 2 and Observables: Data Sharing in a Multi-View Application

Angular 2 and Observables: Data Sharing in a Multi-View Application

Angular 2 and Observables: Data Sharing in a Multi-View Application thumbnail

Recently, I started on a project to migrate JavaScript to TypeScript in one of Lucidchart’s Angular 2 applications. This application has several components, each with its unique view. For those not familiar with Angular 2, a component is merely an encapsulation of a view on a webpage with its associated functionality and styling; e.g., HTML + JS + CSS. In our application, these components use the same data received from the backend, but present a unique view in each of them. The application’s performance and usability relies on the fastest possible availability of data. Since some of these components are rendered simultaneously, effective data-sharing was a good solution to greatly improve the user-experience.

Naive implementation

Let’s prototype this data-sharing service using TypeScript (JavaScript would look the same):


class SharingService {
    private data1: CustomType1;
    getData1():() => Promise {
       if(goog.isDef(this.data1)){
           return Promise.resolve(data1);
       }
       return Net.fetch().then(data => {
           this.data1 = data;
           return data;
       });
    }
}			

@Component({
   templateUrl: '.html',
   selector:'custom-comp-foo',
})
export class CustomComp implements OnInit {
   data1: CustomType1;
   constructor(private sharingService: SharingService) {}
   ngOnInit() {
       this.sharingService.getData1().then(d => {
           this.data1 = d;
       });
   }
}

There are several things to note in the above code snippet.

  • There is a class SharingService which acts like the data-sharing service.
  • The SharingService class is injected into a component CustomComp using Angular 2’s dependency injection framework.
  • The data is obtained from the SharingService by the CustomComp during its initialization (ngOnInit).

The idea of a sharing service here is quite simple. It has a method getData1 that returns a Promise of the data you are interested in and a member variable data1 to store the data. Any other component that’s interested in data1 will have a resolved Promise ready to serve up data1. The following figure better explains the flow of data:


Data Flow between back-end services and components
Data-Flow between back-end services and components

While this implementation is straightforward, it’s not perfect. When data1 is fetched by a component (say component A) once, it remains the same throughout the lifetime of the component. When the sharing service fetches the data again for another component (say component B), this new data is not available for component A, unless component A polls for it or if component A is restarted. Communication between components A and B to know if the data needs to be loaded again can be painful, complicated, and difficult to scale.

Observables: Promises on Steroids

While a Promise represents a value to be resolved in future, an Observable represents a stream of values throughout. An Observable may be completed, which means it won’t emit any further values. An Observer subscribes to these Observables. These Observers are essentially callbacks to emissions of the Observable. This paradigm supports asynchronous operations naturally. In our application, the Angular 2 components have functions which act as Observers, while the data-sharing service can act as an Observable.

Defining Data Sources with Subjects

But since the data-sharing service is not the actual source of the data, Observables are not enough. Our data-sharing service would need to observe the data source (in our case, some HTTP module) while emitting the fetched data. Hence, we need Subjects. A Subject is both an observer and an observable. This is how it works:


class SharingService {
    private data1= new Subject();
    getData1():() => Observable {
        return this.data1.asObservable();
    }
    refresh() {
        Net.fetch().then(data => {
            this.data1.next(data);
        });
    }
} 
//In Component CustomComp
ngOnInit() {
    this.sharingService.getData1().subscribe(d => {
        if(goog.isDefAndNotNull(d)){
            this.data1 = d;
        }
    });
}

This sharing service has a Subject. We only need the Observable portion of the subject for our components: The asObservable method is used to get the data. We also have another method called refresh. This method uses the Net module to fetch the data from the back-end service and pipes it into the Subject using the next call, to which it reacts by emitting the same value.

Storing the Last Value with BehaviorSubject

The data reaches the component when refresh is called on the sharing service and when the component subscribes using the method getData1. However, this solution still isn’t quite right. A normal Subject will emit only future events to an Observer after subscription. For example, if component B subscribes to the data after it is refreshed once, it might not get any data at all unless it’s refreshed again or it subscribed before data was fetched by the Net module. But there is an easier solution to this problem.

BehaviorSubject solves our last problem; it is a type of Subject which always emits the last emitted value to any new subscriber. Unfortunately, BehaviorSubject needs an initial value. Since our data source is a back-end service, there is no synchronous value to initialize with. Hence, we live with using an undefined as the initial state.  The key benefit in this approach is that when component B initiates a refresh, component A will automatically receive the new data. Component A doesn’t need any kind of messaging system to be informed about new data. The data in component A is ever-changing throughout its lifetime.

Dealing with Update Propagation

There is still one more problem left to be addressed. Since none of the components talk to each other, it’s pretty hard to know when a refresh needs to happen. There is no reason to fetch the data before it’s actually necessary. At the same time, each component shouldn’t need to refresh again and again unless it’s necessary. The simplest solution might be to add a flag to the SharingService, to indicate the availability of data. However, this solution requires that the components know about the internals of our SharingService. A better approach might be to expose a different API to the components that takes care of handling the refresh internally. Here is what it looks like:


class SharingService {
    private data1 = new BehaviorSubject(undefined);
    private fetching: boolean;
    private getData1() {
        return this.data1.asObservable();
    }
    awaitData() {
        if(!goog.isDef(this.data1.getValue()) && !this.fetching){
            this.refresh();
        }
        return this.getData1();
    }
    refresh() {
        this.fetching = true;
        Net.fetch().then(data => {
            this.fetching = false;
            this.data1.next(data);
        },err => {
            this.fetching = false;
            this.data1.error(err);
        });
    }
}

//In CustomCompB
ngOnInit() {
    this.updateData(); // Function that changes data1
    this.sharingService.refresh();
}

Several improvements have been made in the above snippet. The awaitData method is a better solution to the last problem—it decides whether or not it is necessary to fetch new data while returning an Observable of the data source. We also added error handling to the refresh method. The below sequence diagram helps visualize the interactions between all the pieces from our final example:

Sequence Diagram with BehaviorSubject
Sequence Diagram with BehaviorSubject

How Can You Benefit From Observables

To summarize, using the Observable pattern provides the following key benefits while developing complex web applications:

  • Provides an easy-to-use event-like abstraction layer where Observable emissions are synonymous with events
  • Helps develop asynchronous, user-interactive applications efficiently
  • Enables a simple communication mechanism across different parts of the application without introducing explicit dependencies between components

15 Comments

  1. Thank you for the article, overall is a great article, but I would rewrite this sentence:
    “An Observable, in the simplest of terms, is just like a Promise but better.”
    Both are not related and give such affirmation can lead to confusion for reader that are not already familiar with observables.

  2. typo: getData1() should return this.data1.asObservable(); instead of this.data.asObservable();

  3. Sriraam SubramanianNovember 15, 2016 at 12:45 pm

    Thanks for pointing it out! I fixed it!

  4. Sriraam SubramanianNovember 15, 2016 at 12:46 pm

    Thanks for the suggestion. I rephrased it to be better.

  5. We found that you can have a “cold” BehaviorSubject if you create a ReplaySubject with a buffer of 1. Then it doesn’t require an initial value.

  6. Sriraam SubramanianNovember 21, 2016 at 6:10 am

    That might actually solve the initial-state issue. Thanks a lot Brian!

  7. To use Subjects and BehaviorSubject which angular module i need to import ?

  8. Sriraam SubramanianFebruary 8, 2017 at 9:59 am

    BehaviorSubject and Subject can be imported from rxjs module like:

    import {BehaviorSubject} from 'rxjs/BehaviorSubject';

  9. Great article Sriraam. I’m struggling to make this work within a polling scenario where current data is immediately available to subscribers but will be refreshed every 30 seconds. Any pointers?

  10. Thank you for the article!!
    Where can I get the NET module? Is it a requirement or how can I replace it?

  11. Fabio Bot SilvaSeptember 26, 2017 at 9:07 am

    Great article!
    It’s a very nice piece of code, very well explained.
    Thank you for share your knowledge.

  12. it helped me! Thanks!!)

  13. Sriraam SubramanianNovember 8, 2017 at 11:19 am

    @mchid
    I am not sure if I understand your concern. But from your description, if current data is a number,

    
    class PollingData {
        private _data = new BehaviorSubject(undefined);
        public readonly data = _data.asObservable().filter(v => v !== undefined);
        update(newValue: number) {
            this._data.next(newValue)
        }
    }
    
    // subscribing to it.
    
    val polling = new Polling(); // or use Angular's Dependency injection to get an instance.
    polling.data.subscribe( (value) => {
        // do something with value
    })
    
    // update the data after 30 seconds
    
    setInterval(() => { 
        let newValue = getPolledData();
        polling.update(newValue); // This should set the value to be 
    }, 30000);
    
    
  14. In your setup, what is the `this.data1.error(err);` suppose to do?

    I have a similar setup, but have had to refactor quite a bit as calling BehaviorSubject.error() cancels all subscriptions to that subject. So no further communication is possible between that subject and any views that may be subscribed. I was attempting a retry of the Http connection, and then calling BehaviorSubject.next(result) – which was how I spotted the issue.

    Wondering what your process is for handling this?

  15. Sriraam SubramanianDecember 20, 2017 at 11:41 am

    `this.data1.error(err)` was meant to do exactly what you described. But in my application, I assume that any response other than a 200/404 is terrible(even once) and I close the subscription, indirectly communicating with the views. This works for my application.

    The process to handle this should be based on what your application needs.

    1) If your views don’t care about retries, you can choose to retry with back-off for n-times. However, if you failed after n-times, it means the user has probably gone offline or if the server is facing issues. If your application can afford a refresh for any of these scenarios, I say, we do `this.data1.error(err)` and severe all subscriptions.

    2) If your views care about retry or if your application cannot afford a refresh, the best thing would be to use a complex messaging format for communication between the data-service and the subscribed views. `this.data1.error(err)` option may not work for you here.

    Hope this answers your question!

Your email address will not be published.