Angular multiple subscriptions when navigating between routes

41 Views Asked by At

I've noticed i'm getting multiple subscriptions when I navigate between routes in my angular app.

In my parent component I have some routes like this:

<li routerLink="route1" [routerLinkActive]="['active']">Route 1</li>
<li routerLink="route2" [routerLinkActive]="['active']">Route 2</li>

Those two routes both load the same child component which then gets some data from my service:

component.ts

this._entityService.getEntities(this.jobId, this.selectedStage)

Then in my service i'm doing this:

service.ts

// get entities
this.http.get(`/api/entities/${jobId}/${stage}`).pipe(take(1)).subscribe((entities:IEntity[]) => {
  this.entities$.next(entities)
})

// get tasks
this.http.get(`/api/tasks/${jobId}/${stage}`).pipe(take(1)).subscribe((tasks:ITask[]) => {
  this.tasks$.next(tasks)
})

combineLatest(
  this.entities$,
  this.tasks$,
).pipe(
  map(([entities, tasks]) => {
    // doing some stuff here
  })
).subscribe(entities => {
  this.entities = entities
})

Then in my components i'm loading the data directly from the service:

html

_entityService.entities

However when I navigate between the two routes the subscriptions aren't being destroyed so I end up with multiple subscriptions. How can I correctly destroy the subscription when I navigate away from the component?

3

There are 3 best solutions below

0
Naren Murali On

We need to add the subscription on the component, thus we can unsubscribe the subscriptions! when the component gets destroyed!

component.ts

...
export class compoenent {
    private sub: Subscription = new Subscription();

    ngOnInit() {
        this.sub.add(
            this._entityService.getEntities(this.jobId, this.selectedStage).subscribe()
        );
    }

    ngOnDestroy() {
        this.sub.unsubscribe();
    }
    ...

service.ts

getEntities(jobId: any, stage: any) {
    // get entities
    const entities$ = this.http.get(`/api/entities/${jobId}/${stage}`).pipe(take(1))
    
    // get tasks
        const tasks$ = this.http.get(`/api/tasks/${jobId}/${stage}`).pipe(take(1))
    
    return combineLatest(
      this.entities$,
      this.tasks$,
    ).pipe(
      map(([entities, tasks]) => {
        // doing some stuff here
      }),
      tap((res: any) => {
          this.entities = entities;
      });
    )
0
OZ_ On

It's because your service is a Singleton (shared across the whole app), but you are using it like a new instance every time. If different components might have different parameters (and might need different combinations of entities and tasks), then this service should not be a singleton.

You can read more about it in my free article: Globally Shared Instances in Angular — Use with Care.

In any case, a Service should not return a subscription or subscribe itself, it should return an Observable. Consumers will decide when to subscribe and when to unsubscribe.

To destroy a subscription when a component is destroyed, you can use operator takeUntilDestroyed().

0
BizzyBob On

I agree with OZ_, your services should not subscribe, but rather just return an observable. In this case, it's probably easiest if you let the service be responsible only for making the http call, and move the combineLatest into your component in order to build up the data your view needs:

// service
getEntities(jobId: string, stage: string): Observable<IEntity[]> {
   return this.http.get(`/api/entities/${jobId}/${stage}`);
}

getTasks(jobId: string, stage: string): Observable<ITask[]> {
   this.http.get(`/api/tasks/${jobId}/${stage}`);
}
// component
vm$ = combineLatest([this.jobId$, this.selectedStage$]).pipe(
  switchMap((jobId, stage) => forkJoin({
    entities : this.service.getEntities(jobId, stage),
    tasks    : this.service.getTasks(jobId, stage),
  }),
});

In the component, we've created a single observable the emits exactly that data needed by the view in the shape: { entities: IEntity[], tasks: ITask[] }. It will emit a new value whenever the jobId$ or the selectedStage$ emit a new value.

jobId$ and selectedStage$ could be Subjects that you call .next() on at the appropriate times. Or... even better if they come from some observable source like the ActivatedRoute or a FormControl, you can declare them as such:

example:

private jobId$ = this.activatedRoute.paramMap.pipe(
  map(params => params.get('jobId')
);

private selectedStage$ = this.form.get('stage').valueChanges;

Notice we haven't subscribed yet, we simply declared the shape and behavior of our view model observable. You can now easily use the async pipe in the template which will handle subscribing and unsubscribing from this observable when the component is created and destroyed:

<div *ngIf="vm$ | async as vm">

  <ul>
    <li *ngFor="let entity of vm.entities">{{ entity.label }}</li>
  </ul>

  <ul>
    <li *ngFor="let task of vm.tasks">{{ task.label }}</li>
  </ul>

</div>