Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Should I use multiple BehaviorSubject for different subscriptions?

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
    }
}
like image 503
Jack Avatar asked May 21 '19 12:05

Jack


2 Answers

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.

like image 152
Yash Rami Avatar answered Nov 15 '22 08:11

Yash Rami


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.

like image 29
Jeffrey Roosendaal Avatar answered Nov 15 '22 08:11

Jeffrey Roosendaal