0
votes

Following the cached service pattern I'm using a BehaviourSubject subscribed by using a read-only Observable created from it in various components

// credential.service.ts
private _credentialList$: BehaviorSubject<Credential[]>
readonly credentialListObv$: Observable<Credential[]>

...
this. credentialListObv$ = this.credentialList$.asObservable()

...
getCredentialList(...): Observable<Credential[]> {
    return this.http.get<Credential[]>(
      ``
      ).pipe(
      tap(credList => {
        ...
        this._credentialList$.next(credList)
      }),
    )
  }

So from the component I trigger the getCredentialList method to emit the fetched list by the _credentialList$ subject and so all the subscribers to the credentialListObv$ should get the value.

The problem is that when I create my component I will get unwanted subscription when I invoke the getCredentialList method

// credential-list.component.ts

private _credentials$: Observable<Credential[]>

constructor(
    private credentialService: CredentialService,
  ) {
    this._credentials$ = this.credentialService.credentialListObv$.pipe(
      tap(() => {console.log("sub")}),
      ...
    )
  }

  ngAfterViewInit(): void {
    this.credentialService.getCredentialList(...).subscribe()
  }

  get credentials$() {
    return this._credentials$
  }

// credential-list.component.html
<mat-expansion-panel *ngFor="let credential of credentials$ | async; let i=index">
        ...

The result is that when I access the page "sub" will be printed twice. The first subscription happens from the async pipe inside the component template but why there is another one if I will never invoke subscribe on this._credentials$ from credential-list component?

2

2 Answers

0
votes

Because you use BehaviourSubject, the first emition is the initial value of it. Then, later, in NgAfterViewInit, you subscribe to getCredentialList which pushes a new value to the same BehaviourSubject.

Try to log the values, instead of typed string:

tap((val) => {console.log(val)}),

The first should be the default one that you used when instantiated subject (this._credentialList$ = new BehaviorSubject(...)

0
votes

That's because you're also subscribing to the service method that returns the HTTP observable. So now your component has two active subscriptions to one http call.

HTTP methods in Angular complete after emitting one value, so you can safely subscribe to the observable inside the method.

getCredentialList(...): Observable<Credential[]> {
    this.http.get<Credential[]>(
      ``
      ).pipe(
      tap(credList => {
        ...
        this._credentialList$.next(credList)
      }),
    ).subscribe();
  }

Then in your component, leave your observable the same, but don't subscribe to the method.

ngAfterViewInit(): void {
  this.credentialService.getCredentialList(...);
}

Now invoking the service method triggers the HTTP request, and your private _credentials$ observable still gets the data from the BehaviorSubject.


Edit: Since you said you don't want to subscribe to anything inside the service, I can offer you a solution that only requires one subscription in the component template.

// credential.service.ts
private credentialParams$ = new Subject<ApiParameters>();
public credentialListObv$: Observable<Credential[]>;

...
this. credentialListObv$ = this.credentialParams$
.pipe(
  switchMap(urlParameters=>
    this.http.get<Credential[]>(`url with ${urlParameters}`)
  )
);

...
getCredentialList(newParams:ApiParameters): void {
  this.credentialParams$.next(newParams);
}

Use the subject to emit whatever parameters you need for your API call. Then the observable takes that emitted parameter and directly returns the HTTP request observable.

// credential-list.component.ts

public credentials$: Observable<Credential[]>

constructor(
    private credentialService: CredentialService,
  ) {
    this.credentials$ = this.credentialService.credentialListObv$.pipe(
      tap(() => {console.log("sub")}),
      ...
    );
  }

  ngAfterViewInit(): void {
    this.credentialService.getCredentialList(apiParams);
  }

// credential-list.component.html
<mat-expansion-panel *ngFor="let credential of credentials$ | async; let i=index">

In the component, we only subscribe to the service observable. On the init lifecycle, we invoke the service method. This causes your component observable to react to the HTTP request that will be returned from the service observable.

P.S. It is okay to subscribe to something within a service, if you know that observable completes (like HTTP requests).