Angular 2 and Observables: Data Sharing in a Multi-View Application
Sriraam Subramanian
Reading time: about 9 min
Topics:
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 componentCustomComp
using Angular 2's dependency injection framework. - The data is obtained from the
SharingService
by theCustomComp
during its initialization (ngOnInit
).
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:
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 whenrefresh
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 theSharingService
, 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:
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.