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