7

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
    }
}
Jack
  • 1
  • 21
  • 118
  • 236
  • What do you mean by 'both methods are called at the same time'? – Plochie May 21 '19 at 12:45
  • I added Component's subscribe methods to my question. 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). – Jack May 21 '19 at 13:00
  • May I ask why you use the same observable for two datatypes (tickets and files)? Doesn't it break as soon as you replace the '' with the correct interface? Also why does it update itself after receiving a new value? This will essentially create an infinite loop, and that's the source of the problem. I'm not very sure what you're trying to achieve either with this pattern. – Jeffrey Roosendaal May 22 '19 at 11:24
  • @JeffreyRoosendaal I simply use a service in order to call method between two components they have no parent-child relations. However, I am not sure if I use separate BehaviorSubject instances for each method or for each component. For example Component A calls X method from Component C via this service and similarly Component B calls Y method from Component C via this service as well. In that case should I use 2 BehaviorSubject instances? Or can I perform this by using the approach above? Could you pls post a suitable example by using the code in my question? Thanks in advance. – Jack May 23 '19 at 11:19
  • @Willys I've posted a long answer. Not addressing your issue per se, but I hope it will help to understand *why* you have this problem. Let me know if it helps. – Jeffrey Roosendaal May 23 '19 at 13:50
  • Take a read of my article on a library I wrote https://github.com/adriandavidbrand/ngx-rxcache to help you with scenarios like this https://medium.com/@adrianbrand/angular-state-management-with-rxcache-468a865fc3fb. It manages all the BehavourSubjects for you and lets you get on with the rest of the app. – Adrian Brand May 29 '19 at 23:17
  • @AdrianBrand Thanks a lot for your helps... I wil look at that page, but before that I have created an approach by combining some approaches as you see on Update section of the question. Could you please have a look at and let me know if it is a suitable usage **in order to call a method between sibling components (components with no parent or child relationships)**. If there is mistakes of you have suggestions, could you pls post them? Thanks in advance... – Jack May 30 '19 at 06:22

2 Answers2

5

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.

Yash Rami
  • 2,276
  • 1
  • 10
  • 16
  • Thanks a lot for your help, but I am looking a solution by **using BehaviorSubject**. Any idea? Regards... – Jack May 21 '19 at 13:02
  • Yes, Just replace the `subject` with `BehaviorSubject` I just update my answer please check. :) – Yash Rami May 21 '19 at 13:05
  • Could you pls post your answer using my code in the question? – Jack May 21 '19 at 13:11
  • Thanks a lot, votep up. But, I am wondering what about the filter method using on [BehaviorSubject subscriber gets same next() element multiple times](https://stackoverflow.com/questions/51478183/behaviorsubject-subscriber-gets-same-next-element-multiple-times)? Is that more cleaner? – Jack May 21 '19 at 14:12
  • Yes, It is but in some case this way is not suitable. for example if i have a 3 component `comp1`, `comp2` and `comp3` and they all are rendered and they dont have any parent child relationship. If i want to send data from comp1 to comp3 then `ngDoCheck()` or `ngChanges()` is not trigger. – Yash Rami May 21 '19 at 14:22
  • memory leak? I used exact same logic in my project but m worried if will create mutltiple subscriptions and memory leak – minigeek Mar 29 '21 at 10:45
3

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.

Jeffrey Roosendaal
  • 6,872
  • 8
  • 37
  • 55
  • Thanks a lot for your answer. I just have time to examine your answer. Regarding to your answer, could you pls clarify me about the following issue? >>> **1)** Is the usage on [How to execute a function from another component that is NOT a sibling of the first component?](https://stackoverflow.com/questions/48069554/how-to-execute-a-function-from-another-component-that-is-not-a-sibling-of-the-fi) is wrong? Because you said "never use this construction, because it creates an infinite loop" but the usage at here is same as my usage in this question. Is there any problem? – Jack May 28 '19 at 13:32
  • **2)** I think on that example, there is a lack regarding to "completing the observables in your component". Is that true? – Jack May 28 '19 at 13:32
  • @Willys Of course! 1) The accepted answer has the EventProxyService, and it shows a seperate update function just like I recommended. What you did was essentially "update on update", there may be a built-in mechanism te prevent the loop, but I don't recommend it. 2) Yes, always complete *custom* observables (like yours with the Subject). Angular observables complete themselves however (HttpModule, ReactiveForms, etc) – Jeffrey Roosendaal May 29 '19 at 10:14
  • Many many thanks for your kind help. Finally I have created an approach by combining some approaches as you see on Update section of the question. Could you please have a look at and let me know if it is a suitable usage in order to call a method between sibling components (components with no parent or child relationships). If there is mistakes of you have suggestions, could you pls post them? Regards... – Jack Jun 15 '19 at 22:07
  • @Willys Sorry for the late reply. Well, if your code works now, it's fine! (It does now, right?) You called your Service 'proxy', but personally I think a Service is *always* some kind of proxy (a bridge between components, actually). In general, I try to avoid almost all logic in a component, and let a Service do the job, so the data can easily be shared among multiple components, and it also makes refactoring a lot easier. When using (lots of) services, just be aware of circular dependecies! – Jeffrey Roosendaal Jun 17 '19 at 13:12
  • No problem. The approach works, but I just wanted to be clarified if **using a single BehaviorSubject" for multiple subscription is good. As we unsubscribe in the proper section, I think there is no possibility to mix them or subscribe multiple times to the same object. Any idea? – Jack Jun 18 '19 at 13:35
  • @Willys I would definitely create multiple. I recommend trying to make your code *single purpose* where possible (a separate service for every data type). You may end up with more code, but with less (and often easier to fix) bugs. Also, when you add a Type to your Subject (like Subject), you cannot even use it for multiple data types. See the section in my answer about Types. So: please do not use a single Subject for everything. – Jeffrey Roosendaal Jun 18 '19 at 19:44
  • Ok, thanks a lot for your valuable suggestion. I will update by creating multiple subjects for each call between components. In this case should I repeat **eventTracker=...**, **getEvent(){}** and **setEvent(){}** part for for each call between components (in this example reloadComment() and listComment())? – Jack Jun 18 '19 at 20:06
  • @Willys Yes, I would :) – Jeffrey Roosendaal Jun 18 '19 at 20:08
  • Many many thanks for your helps, marked as answer... Just a last question: as the project grows, in this case there are too many new subjects in the related services. In this case would it be better to use a single subject for every 2-3 different calls? It might also group the related calls between components and there is no collision between these calls as the subscriptions are unsubscribed onNgDestoy() s. Any idea? – Jack Jun 18 '19 at 20:28
  • @Willys No problem, glad to help! Well, no, I would still create a new Subject for *every different data type*. It’s not uncommon for a larger application to have dozens of Subjects, but only use one or two per dedicated service, so it stays clean and single purpose. As long as you unsubscribe, you should be fine. Good luck! – Jeffrey Roosendaal Jun 18 '19 at 20:40