I have some sibling components and a DataService in my Angular (v7) project and I call methods as the following scenario:
TicketComponent adds ticket and calls reloadTickets method in TicketListComponent and similarly FileComponent adds file and calls reloadFiles method in FileListComponent via DataService as shown below:
DatasService.ts:
export class DatasService {
private eventSubject = new BehaviorSubject<any>(undefined);
getEventSubject(): BehaviorSubject<any> {
return this.eventSubject;
}
reloadTickets(param: boolean) {
this.eventSubject.next(param);
}
reloadFiles(param: any) {
this.eventSubject.next(param);
}
}
TicketComponent:
ngOnInit(): void {
this.dataService.getEventSubject().subscribe((param: any) => {
this.reloadTickets();
});
}
FileComponent:
ngOnInit(): void {
this.dataService.getEventSubject().subscribe((param: any) => {
this.reloadFiles();
});
}
When I use single BehaviorSubject for these 2 methods, both methods are called at the same time when one of them is called. I mean that As both of them subscribed via getEventSubject() method, reloadTickets() methods also triggers reloadFiles() in the DataService as both of them use the same subject (eventSubject). I know creating another BehaviorSubject and getEventSubject method fix the problem but I am confused if I should do this for all of the independent method calls or if there is a smarter way to fix the problem via using single BehaviorSubject as mentioned below:
BehaviorSubject subscriber gets same next() element multiple times
Could you please post a proper usage for this scenario?
Update:
Finally I have used the following approach in order to call different methods between different components using a single BehaviorSubject.
EventProxyService:
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
@Injectable()
export class EventProxyService {
private eventTracker = new BehaviorSubject<any>(undefined);
getEvent(): BehaviorSubject<any> {
return this.eventTracker;
}
setEvent(param: any): void {
this.eventTracker.next(param);
}
}
CommentComponent: Call the method from ListComponent after a comment is added:
import { EventProxyService } from './eventProxy.service';
export class CommentComponent implements OnInit {
constructor(private eventProxyService: EventProxyService) {}
public onSubmit() {
//...
this.reloadComment(true);
}
reloadComment(param: boolean): void {
this.eventProxyService.setEvent(param);
}
}
ListComponent: Triggered via reloadComment() method in CommentComponent :
import { EventProxyService } from './eventProxy.service';
export class ListComponent implements OnInit {
subscription;
constructor(private eventProxyService: EventProxyService) {}
ngOnInit() {
this.subscription = this.eventProxyService.getEvent().subscribe((param: any) => {
this.listComment(param);
});
}
// Multi value observables must manually unsubscribe to prevent memory leaks
ngOnDestroy(): void {
this.subscription.unsubscribe();
}
listComment(param) {
//retrieve data from service
}
}
Yes there is one smarter way to create a BehaviorSubject dynamically here is the example. I hope it helps you out.
1./ DatasService.ts
interface Event {
key: string;
value: any;
}
@Injectable({
providedIn: 'root'
})
export class Broadcaster {
// subject
protected _eventsSubject = new BehaviorSubject<any>(undefined);
constructor() {
}
broadcast(key: any, value: any) {
this._eventsSubject.next({ key, value }); // here we are setting the key and value of our subject
}
on<T>(key: any): Observable<T> {
return this._eventsSubject.asObservable()
.pipe(
filter(e => e.key === key),
map(e => e.value)
);
}
}
2./ TicketComponent
// this is a component which consume the same BehaviorSubject but we are getting a value from "ticket" key
import { Broadcaster } from '../BrodcastService.service';
export class ComponentOne implements OnInit {
constructor(private broadcaster: Broadcaster) { }
someFunction() {
//"ticket" is our key name. so we are getting a value of that key only
this.broadcaster.on('ticket').subscribe(response => {
console.log(response); // here you are getting the data from the other component
});
}
3./ FileComponent
// this is a component which consume the same BehaviorSubject but we are getting a value from "file" key
import { Broadcaster } from '../BrodcastService.service';
export class componentTwo implements OnInit {
constructor(private broadcaster: Broadcaster) { }
someFunction() {
//"file" is our key name. so we are getting a value of that key only
this.broadcaster.on('file').subscribe(response => {
console.log(response); // here you are getting the data from the other component
});
}
So if you want to send data for ticket component then component which send the data for ticketcomponent
import { Broadcaster } from '../BrodcastService.service';
export class ComponentOne implements OnInit {
constructor(private broadcaster: Broadcaster) { }
someFunction() {
this.broadcaster.broadcast('ticket', 'data for ticket');
}
component which send the data for filecomponent
import { Broadcaster } from '../BrodcastService.service';
export class ComponentOne implements OnInit {
constructor(private broadcaster: Broadcaster) { }
someFunction() {
this.broadcaster.broadcast('file', 'data for file');
}
So basically we are creating only one BehaviorSubject but that BehaviorSubject contain a multiple object which are storing our data and we access the data by using a key in your case we have key name like file and ticket.
It's hard for me to know what you're actually trying to achieve, but..
First, never use this construction, because it creates an infinte loop:
this.dataService.getEventSubject().subscribe((param: any) => {
this.reloadTickets();
});
When the value changes, you have access to the new values in the component. You should only update the observable once you manipulated your data, like:
// Reads the observable
this.dataService.getEventSubject().subscribe((param: any) => {
this.populateForm();
});
// Updates the observable
this.addTicket() {
this.dataService.addTicket()
}
Next, you should always type your variables, for example:
export interface Ticket {
artist: string;
price: number;
}
export interface File {
name: string;
type: 'gif' | 'jpg' | 'png';
}
As soon as you add the types to the Observable, you notice that you actually need two Subjects.
// As a convention, It's recommended to use singular form, and add a $.
public ticket$ = new BehaviorSubject<Ticket[]>(null);
public file$ = new BehaviorSubject<File[]>(null);
Also, I should make them public, to have easy access without needing a get(). You can simply access it by injecting the service and calling the observable.
constructor(
private dataService: DataService
)
this.dataService.ticket$
When you need to make them private, you should use:
private _ticket$: Subject<Ticket[]> = new BehaviorSubject<Ticket[]>(null);
public ticket$ = this._ticket$.asObservable();
With that construction, you can read the observable in every service/component, but only update them in the containing service.
Another thing you should always do is complete the observables in your component, otherwise you keep an open subscription forever:
private destroy$ = new Subject<any>();
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
this.dataService.ticket$.pipe(takeUntil(this.destroy$)).subscribe(tickets => {
// Do something
})
Bottom line: When you follow the right patterns, you will get a lot less issues/bugs.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With