Angular Subjects: See how to efficiently use them by implementing an intermediate service for data sharing between components
231110
Intro
Using a shared service to exchange data between 2 unrelated components is a quite used practice. Here, we will go coding in a step-by-step manner, to see how we can use the Subject’s flavors and avoid some commonly faced pitfalls.
Preparatory notes regarding our Shared Service
Subjects
The most important point is to use a Subject for our message to be exchanged. A Subject is a special type of Observable that shares a single execution path among observers. While plain Observables are unicast (each subscribed Observer owns an independent execution of the Observable), Subjects are multicast, which means that it allows values to be multicasted to many Observers.
In our case, there is also another benefit of using a Subject instead of observable. A Subject exposes the .next() function which allows us to manually trigger emissions with the parameter of the next function as the value.
A Subject is quite convenient in cases when we want to get just the value returned (the current value), by using another of its exposed functions, the .getValue(). However, we can still use a Subject as an Observable, which means that we can use the ‘formal’ way to subscribe to it as we do with any other observable and keep getting any new emitted values.
Furthermore, a Subject has a couple of flavors such as the BehaviorSubject, which is a special Subject that requires an initial value, and also, (important too) emits its current value (last emitted item) to any new subscribers. Other flavors are the ReplaySubject (which will be seen later on in this post), as well as the AsyncSubject.
Know the sender
In our example case, we are going to use a bi-directional communication between 2 or more components, so we need to know the sender of the message, and the source that notifies the shared service providing a message. This will allow us to capture and display in a component only messages that are coming from other component sources, and exclude the messages initiated from this particular component to also return back to it. For that purpose, we are going to use a very simple interface:
export interface IMsg { sender: string; msg: string; }
Practically, this means, that instead of exchanging simple string messages, we will exchange IMsg objects. As a sender, we can use any token we wish that distinguishes a particular sender from the others. However, for simplicity, here, we are going to use each component name, which can be easily automatically obtained using the component constructor name with a property, e.g.:
public compName = this.constructor.name;
The initial Code
Finally worth noticing, that our shared service will have just 2 simple methods for setting and getting the messages (IMsg objects).
The ShareDataService Using just a Subject
The Test1Component
The code of Test2Component is pretty similar to the above one.
You can find the so-far repo in GitHub here, and you can also see it at Codesandbox here. Or play with the running app here.
Challenges
Generally, the good practice is to obtain values from a service observable, by making our subscription within ngOnInit lifehook. However, when we subscribe in the ngOnInit lifehook, there are times when the emitted value from a service has already gone. In other words, the subscription to an observable from within the ngOnInit life hook might be delayed to capture a value from an observable of that service. This is especially true, for events (e.g. messages sent) initiated from a parent component upon the parent’s instantiation. It is also true for unrelated (e.g. sibling components) where the initiator component is created first and sends a message upon its creation, e.g. from its constructor or from its ngOnInit method.
In our case, we have 2 components Test1Component and Test2Component that are sibling components under the root component which is the AppComponent here.
In Angular, the order of component creation (instantiation) is determined by the structure of the component tree, which is defined by the parent-child relationships between components.
The general ordering that Angular uses to create components is:
1. The root component
This is usually the AppComponent, and it is created first. This is the top-level component and serves as the root of the component tree.
2. The Parent Components.
After the root component, parent components are created. If a parent component has multiple child components, the parent is created before any of its children.
3. The Child Components.
Child components are created after their parent component. If a component has multiple child components, they are created in the order they are declared in the parent’s template.
4. Sibling Components.
Sibling components (components that share the same parent) are created in the order they are declared in the parent’s template.
This order of creation also determines the order in which Angular runs lifecycle hooks for each component. For example, the `ngOnInit` hook of a parent component will run before the `ngOnInit` hook of its child components. We can see and check it, also in our case. If we put a statement sending an initial message from within the ngOnInit lifehook of the Test1Component, we will see that this message has been transmitted before the initialization of the Test2Component, and thus the Test2Component has lost it. But when we do the same in the 2nd component (sending an initial message from within the ngOnInit lifehook of the Test2Component), the message is captured and displayed OK from the Test1Component.
This is because the Test1Component has been created first and the Test2Component second. Since they are sibling components, the creation order has been determined from the order we use them in the AppComponent’s template.
Now if we just change the order in the AppComponent’s template, we will see that we get the analogous result:
Solutions
So, what can we do to deal with that? Perhaps there are more options, but here we will see just 2 of them:
- Subscribing into the Constructor
- Using a BehaviorSubject or a ReplaySubject
Subscribing into the Constructor
This is an alternative that in most cases solves the issue for the ‘consumer’ component, which wants to get the message from the service. Subscribing from within the constructor method means that the subscription takes place quite early. At the same time, the component creation/instantiation, and thus it succeeds in obtaining the emitted value from the service via the subscription.
Of course, this is true for any piece of code that is put into the component’s constructor. This is due to the fact that the bootstrap process of Angular comprises the following two key phases:
- Components tree construction
- Change detection process
The components tree construction is when the Angular scans the components, and instantiates each one (in the order we’ve mentioned before) on the basis of each component constructor because it is necessary to take into account any dependencies that are needed for the particular component.
So, the constructor code precedes the ngOnInit life hook, which follows in the change detection phase.
One should be aware that the general execution order for a Component in Angular is the one given below:
- Constructor
- ngOnChanges
- ngOnInit
- ngDoCheck
- ngAfterContentInit
- ngAfterContentChecked
- ngAfterViewInit
- ngAfterViewChecked
- ngOnDestroy
That said, we can update our code, accordingly. So, the code for the Test2Component is given below.
Test2Component
If you run the updated code for the Test2Component, you will see that it works OK:
Even if the above is a working solution, it might be not the best one, especially when it is necessary to use other properties within our subscription, that are only obtained during the change detection phase, e.g.: input binding properties via @Input() decorator, elements obtained via @ViewChildren, etc.
[Note: @Input properties are known in ngOnInit, however for @ViewChildren queries, we need to use the ngAfterViewInit (or ngAfterContentInit) method]
So, another option we have is to use a BehaviorSubject instead of a simple Subject.
Using a BehaviorSubject
The point of a Subject that should be well understood, is that emitted data from a Subject will only reach the subscribers that are already active subscribers. If a subscriber delays subscribing, it loses any prior emitted data.
As we’ve already mentioned, a BehaviorSubject, is nothing but a special Subject that requires an initial value, and also, keeps and emits its most recent/last value to any newcomer subscriber. Practically, this means that a BehaviorSubject keeps its last emitted value, so any delayed subscriber-client that starts subscribing/observing it can obtain this most recent (last emitted) value, upon its subscription.
So, for our case, we can benefit from using a Behavior Subject and succeeding in capturing the initial message from within ngOnInit. Thus, we can put back our subscription in the ngOnInit, and adapt the code of our service, which now becomes:
The updated ShareDataService using a BehaviorSubject
As you can see above, the only change is about the BehaviorSubject.
However, note that there is no warranty that we will be able to capture the initial message under all circumstances. It always depends on how fast a component is instantiated and when the subscription starts. If meanwhile, there were other messages sent, they perhaps will be lost. So, what is important to remember is that when using a BehaviorSubject, a new subscriber will always receive only the last emitted data.
You can find the so-far repo in GitHub here, and you can also see it at CodeSandbox here. Or play with the running app here.
Using a ReplaySubject
The ReplaySubject is pretty similar to a BehaviorSubject, however, it can be configured to keep more emitted items in its buffer instead of just the last one. We can also define how long time an emitted value will be kept in the buffer. Thus, a ReplaySubject takes 2 parameters that can be coexisted. For instance, if you would like to buffer a maximum of 5 values, as long as the values are less than 3 seconds old, you could do it like this:
public messageSubject = new ReplaySubject<IMsg>(5, 3000);
So, using it, instead of a Behavior subject, gives you more chances to catch up on some earlier emissions, if the client subscription is delayed. The updated code is almost the same as
The updated ShareDataService using a ReplaySubject
A minor change I made, apart from setting the type of messageSubject property to ReplaySubject, is that I used an initial message in the constructor, which is analogous to the initial message in the previous version with BehaviorSubject.
The Test1Component
Because I want to show any previously sent messages captured, I changed the code a bit, as well as the template and the style files accordingly, using the different properties for messages received and messages sent, and in a cumulative manner (every new message is added to the previous messages).
The changes are identical for the Test2Component, as well.
When you run the app you will see now, that all initial messages are captured by both of the components.
If you switch back to the BehaviorSubject, you will see that the Tes2Component fails to capture the initial message because its creation is delayed (it is created after the Test2Component).
Of course, you can always use a waiting timer to play around with delays, e.g. in the ngOnInit of the Test2Component:
You will see that the result is the same and the initial messages sent are captured OK, even after 2-seconds delay.
However, if the delay is getting 3 seconds and more you probably will lose all the initial messages
As a final note, let us make just a short reference to the 3rd flavor, the AsyncSubject. The AsyncSubject sends again the last emitted value, as a Subject, but it does it upon its completion, which can be also manually forced by using its .comlete() function.
Conclusion
What actually one should keep in mind as take-offs are:
- Using a mediator/shared service is a quite good practice when we want to exchange data between unrelated components.
- Using a Subject in a shared Service is also a quite convenient way to inform many subscribed components simultaneously (multicasting) when a new event (aka a new value) is emitted, and which can be triggered/fired ‘manually’ via the .next() function by any other component uses the shared service.
- A Subject can be used as observable, which allows us to deal with is with ‘normal’ observable subscriptions.
According to each specific implementation (use case), challenges are always expected. Sometimes there would be missing emitted values, but we can deal with that, by using the other flavors of the Subject family, such as BehaviorSubject and ReplaySubject, as we have seen above.
Find the final repo in GitHub here, and you can also see it at CodeSandbox here. Or if you want just to play with the running app, here.
So, that’s it for now!
I hope you enjoyed it! Stay tuned, and keep coding!