10

How can I use router Observables more efficiently? If I need to load a single route parameter for example (let's say we have a route like /some-resource/:id), I need to subscribe to the router event, then to the route params to get the value. This requires two subscriptions and two unsubscribes.

I would like to:

  • Reduce boilerplate code
  • Make the code more readable
  • Get rid of subscriptions

Sample

export class SomeComponent implements OnInit, OnDestroy {
  private routerSub: Subscription;
  private routeSub: Subscription;

  someResource: Observable<SomeResourceType>;

  constructor(private someService: SomeService,
              private route: ActivatedRoute,
              private router: Router) {
    this.routerSub = this.router.events.subscribe((event) => {
      if (event instanceof NavigationEnd) {
        this.routeSub = this.route.params.subscribe((params) => {
          if (params['id']) {
            this.someResource = this.someService.findById(params['id']);
            // will access the resource using async pipe later
          }
        });
      }
    });
  }

  ngOnInit(): void {
  }

  ngOnDestroy(): void {
    this.routerSub.unsubscribe();
    this.routeSub.unsubscribe();
  }
}

The event subscription is needed to refresh the data if for some reason the component is not destroyed by angular, but still loaded using a different route param stackblitz example: https://stackblitz.com/edit/angular-router-basic-example-695kpb

szab.kel
  • 2,356
  • 5
  • 40
  • 74

4 Answers4

9

You can use activated route for that.

constructor(route: ActivatedRoute) {
    this.id$ = route.params
        .pipe(pluck('id'));
}

You can use pluck. pluck('id') is basically the same as map(value => value.id). If you don't want to have a stream but the actual value, you can do the same and subscribe to it. But if you do that, don't forget to unsubscribe from the observable. You can do this with the take until operator.

id;
private _destroyed$ = new Subject<any>();

constructor(route: ActivatedRoute) {
    route.params
        .pipe(
            takeUntil(this._destroyed$),
            pluck('id')
        ).subscribe(id => this.id = id);
}

ngOnDestroy() {
    this._destroyed$.next();
    this._destroyed$.complete();
}
tom van green
  • 1,674
  • 10
  • 14
  • Why do we need the `_destroyed$`? The `subscribe` method is called, so we still need to add a subscription, which we can unsubscribe from on destroy. – szab.kel Sep 21 '18 at 05:42
  • 1
    We need the _destroyed$ so that we don't need to unsubscribe. The takeUntil operator unsubscribes as soon as our component is destroyed. It might not make a big difference when you have only one subscriptions, but if you have many subscriptions in your component it is much cleaner to do it this way. – tom van green Sep 21 '18 at 08:52
  • Have a read in the accepted answer here: https://stackoverflow.com/questions/38008334/angular-rxjs-when-should-i-unsubscribe-from-subscription?rq=1 . – tom van green Sep 21 '18 at 08:54
7

Try this:

constructor(private route: ActivatedRoute) {}

ngOnInit() {
    const id = this.route.snapshot.params['id'];
}
tano
  • 2,657
  • 1
  • 15
  • 16
  • 1
    As I mentioned in my question, this won't handle the navigation event, so if the URL changes, your component won't be destroyed or re initiated, meaning copy-pasting URLs might not work. – szab.kel Sep 20 '18 at 13:45
  • 1
    If you change the url by overwriting it, the app will be reloading. You can reload the app, it works since it is read in ngOnInit. – tano Sep 20 '18 at 13:50
  • You are right about the by manual change, but not if you are using routerLinks. Check this out: https://stackblitz.com/edit/angular-router-basic-example-695kpb (go to the catalog) – szab.kel Sep 20 '18 at 14:15
  • The problem mainly is, Angular won't destroy and recreate your component if you are routing to the same component. Meaning you have to specifically wait for such an event and change your state yourself. Feel free to enlighten me if I am missing something. – szab.kel Sep 20 '18 at 14:22
1

As long as no one posts a better solution, here is mine:

I defined a RouterHelperService, which makes this process somewhat easier. One of the problem is, if you try to inject the ActivatedRoute instance directly in your service, you will be missing the params, so you need to pass it from your component to the service.

import { Injectable } from '@angular/core';
import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
import { filter, flatMap, map } from 'rxjs/operators';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class RouterHelperService {

  constructor(private router: Router) {
  }

  onNavigationEndReadParamByKey(route: ActivatedRoute, key: string): Observable<string> {
    return this.router.events.pipe(
      filter(event => event instanceof NavigationEnd),
      flatMap(() => {
        return route.params.pipe(
          filter(params => params[key]),
          map(params => params[key])
        );
      })
    );
  }
}

This way, in my component, I can call it with a single call and a single subscription.

export class SomeComponent implements OnInit, OnDestroy {
  private routeSub: Subscription;

  someResource: Observable<SomeResourceType>;

  constructor(private someService: SomeService,
              private route: ActivatedRoute) {
     this.routeSub = this.routerHelper.onNavigationEndReadParamByKey(this.route, 'id').subscribe((id) => {
        this.someResource = this.someService.findById(+id); //+id to convert it from a string to a number
     });
  }

  ngOnInit(): void {
  }

  ngOnDestroy(): void {
    this.routeSub.unsubscribe();
  }
}
szab.kel
  • 2,356
  • 5
  • 40
  • 74
1

Fastest way, which does not support component reload:

constructor(
  route: ActivatedRoute,
) {
  const myParam = route.snapshot.params.idThread;
}

Using the paramMap and the 'new' RxJs syntax, which support component reload:

constructor(
  route: ActivatedRoute,
) {
  route.paramMap.subscribe({
    next: params => {
      const myParam = params.get('myParam');
    }
  });
}

NB: unsubscribing is not mandatory in this case and should not lead to memory leaks

Ploppy
  • 14,810
  • 6
  • 41
  • 58