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

Sriraam Subramanian

Reading time: about 9 min

Topics:

  • Architecture
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

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

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

About Lucid

Lucid Software is a pioneer and leader in visual collaboration dedicated to helping teams build the future. With its products—Lucidchart, Lucidspark, and Lucidscale—teams are supported from ideation to execution and are empowered to align around a shared vision, clarify complexity, and collaborate visually, no matter where they are. Lucid is proud to serve top businesses around the world, including customers such as Google, GE, and NBC Universal, and 99% of the Fortune 500. Lucid partners with industry leaders, including Google, Atlassian, and Microsoft. Since its founding, Lucid has received numerous awards for its products, business, and workplace culture. For more information, visit lucid.co.

Get Started

  • Contact Sales

Products

  • Lucidspark
  • Lucidchart
  • Lucidscale
PrivacyLegalCookie privacy choicesCookie policy
  • linkedin
  • twitter
  • instagram
  • facebook
  • youtube
  • glassdoor
  • tiktok

© 2024 Lucid Software Inc.