58

I have set up the following routing system

export const MyRoutes: Routes = [
  {path: '', redirectTo: 'new', pathMatch: 'full'},
  {path: ':type', component: MyComponent}
];

and have the following navigation system

goToPage('new');
goToPageNo('new', 2);

goToPage(type) {
  this.router.navigate([type]);
}
goToPageNo(type, pageNo) {
  this.router.navigate([type], {queryParams: {page: pageNo}});
}

Sample URL looks like this

http://localhost:3000/new

http://localhost:3000/new?page=2

http://localhost:3000/updated

http://localhost:3000/updated?page=5

Sometimes they have optional queryParams (page)

Now I need to read both route params and queryParams

ngOnInit(): void {
  this.paramsSubscription = this.route.params.subscribe((param: any) => {
    this.type = param['type'];
    this.querySubscription = this.route.queryParams.subscribe((queryParam: any) => {
      this.page = queryParam['page'];
      if (this.page)
        this.goToPageNo(this.type, this.page);
      else
        this.goToPage(this.type);
    })
  })
}

ngOnDestroy(): void {
  this.paramsSubscription.unsubscribe();
  this.querySubscription.unsubscribe();
}

Now this is not working as expected, visiting pages without queryParams works, then of I visit a page with queryParams "goToPageNo" gets called multiple times, as I am subscribing to queryParams inside route params.

I looked at the Angular 2 documentation, they do not have any example or codes where a subscription to both route params and queryParams is implemented at the same time.

Any way to do this properly? Any suggestions?

BinaryButterfly
  • 18,137
  • 13
  • 50
  • 91
Shifatul
  • 2,277
  • 4
  • 25
  • 37

9 Answers9

53

I managed to get a single subscription to both the queryParams and Params by combining the observables by using Observable.combineLatest before subscribing.

Eg.

var obsComb = Observable.combineLatest(this.route.params, this.route.queryParams, 
  (params, qparams) => ({ params, qparams }));

obsComb.subscribe( ap => {
  console.log(ap.params['type']);
  console.log(ap.qparams['page']);
});
pblack
  • 778
  • 1
  • 6
  • 13
  • Wow, it works, awesome, thanks so much. BTW, wanted to ask you, don't we need to 'unsubscribe' this in ngOnDestroy() function ? – Shifatul Dec 11 '16 at 00:46
  • Well I've not been unsubscribing generally based on [this SO question](http://stackoverflow.com/questions/35042929/do-you-need-to-unsubscribe-from-angular-2-http-calls-to-prevent-memory-leak) but hadn't thought more about combineLatest but believe it will propagate down, [suggested here](http://stackoverflow.com/questions/31122768/will-combinelatest-trigger-doonunsubscribe-of-its-children). In my first month of the love hate relationship with angular 2 (more hate so far tbh :)) so not 100%. – pblack Dec 11 '16 at 19:13
  • 15
    How does it actually help? If I change at the same time both query and path parameters my callback is fired twice. – joycollector Apr 19 '17 at 21:23
  • 3
    import { Observable } from 'rxjs/Observable'; import 'rxjs/add/observable/combineLatest'; – TroodoN-Mike Jul 31 '17 at 10:37
  • 1
    @joycollector any work around for that? I stuck on the same – Shabbir Dhangot Feb 07 '18 at 11:54
  • @ShabbirDhangot actually I switched to react :) – joycollector Feb 13 '18 at 15:56
  • 8
    @ShabbirDhangot I also ran into an issue where combineLatest on route params and fragment emitted two values when navigating. I worked around it by piping the combineLatest observable through debounceTime(1). – Theodore Brown Oct 25 '18 at 16:37
  • @ShabbirDhangot debounceTime(0) should work as well – Vincent Oct 15 '19 at 09:05
  • @joycollector I know it is late. But probably you can use debounceTime rxjs operator which will ignore first emit. Setting value to 1 probably good option if the observables triggerend not asynchronously – simply good Apr 08 '20 at 15:31
  • 1
    @TheodoreBrown, the debounceTime(1) fixed it for me. Thanks! – Daniel Flippance May 26 '22 at 23:35
36

For Angular 6+

import { combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';

...

combineLatest(this.route.params, this.route.queryParams)
    .pipe(map(results => ({params: results[0].xxx, query: results[1]})))
    .subscribe(results => {
        console.log(results);
    });

Where xxx is from your route

{path: 'post/:xxx', component: MyComponent},

Note for Angular 12+ (i.e. RxJs version 7+), use:

import { combineLatest, map } from 'rxjs';

and pass an array to the combineLatest function:

combineLatest([this.route.params, this.route.queryParams])
    .pipe(map(results => ({params: results[0].xxx, query: results[1]})))
    // ...
adelinor
  • 703
  • 9
  • 15
Shifatul
  • 2,277
  • 4
  • 25
  • 37
  • 1
    Note that this, of course, relies on both `import { combineLatest } from 'rxjs';` and `import { map } from 'rxjs/operators';` – Pete Nov 12 '18 at 21:19
  • 5
    but it's fire twice from post/:xxx?page=1 to 'post/:xxx' – mai danh Jan 20 '19 at 10:10
  • @maidanh can u provide an example on stackblitz – Shifatul Jan 21 '19 at 01:50
  • See my link demo https://angular-ev2rns-subscribe-param-and-path-param.stackblitz.io First I hit on cat1, cat2, or cat3. Second I hit on page1, page2, page3. It's okie now. When I hit back to cat1, cat2, cat3 it trigger twice – mai danh Jan 21 '19 at 02:43
  • Open console to see the log – mai danh Jan 21 '19 at 02:44
  • @maidanh this is super wired, i think u should create a new issue about this problem - https://github.com/angular/angular/issues – Shifatul Jan 21 '19 at 05:08
  • 2
    @ThunderRoid I created a post https://stackoverflow.com/questions/54283605/how-to-detect-url-change-in-angular-7-include-param-and-query-param. And I see there was an issue it seem related to it. https://github.com/angular/angular/issues/21124 – mai danh Jan 21 '19 at 05:50
  • @maidanh did you find any solution , it tigers twice – giveJob Feb 01 '19 at 06:28
  • @HemanthSP I worked around with this solution. https://stackoverflow.com/questions/54283605/how-to-detect-url-change-in-angular-7-include-param-and-query-param But there was a comment in that post but I dit not try it yet – mai danh Feb 04 '19 at 08:03
  • I tried that comment it's working fine because I also face same issue – giveJob Feb 04 '19 at 08:28
  • In Angular 9 (maybe earlier too), to avoid a `deprecated` warning, use `combineLatest([this.route.params, this.route.queryParams])` – user2846469 Feb 20 '20 at 16:42
22

Late answer, but another solution : Instead of subscribing to the params and queryparams I subscribe to the NavigationEnd event on the router. Fires only once and both params and queryparams are available on snapshot : (example for angular 4)

this.router.events.filter(event=>event instanceof NavigationEnd)
   .subscribe(event=>{
       let yourparameter = this.activatedroute.snapshot.params.yourparameter;
       let yourqueryparameter = this.activatedroute.snapshot.queryParams.yourqueryparameter;
   });

Regarding unsubscribing : yes it is true routing params, queryParams or navigation events subscriptions are automatically unsubscribed by the router, there is however one exception : if your route has children , when navigating between children, no unsubscribe will occur. Only when you navigate away from the parent route!

E.g. I had a situation with tabs as child routes. In constructor of first tab component subscribing on route params to retrieve data. Each time I navigate from first to second tab and back another subscription was added resulting in multiplying the number of requests.

giveJob
  • 1,500
  • 3
  • 17
  • 29
rekna
  • 5,313
  • 7
  • 45
  • 54
  • This works for the case that either the `param` or `queryParam` changes - and only triggers once if both change. Great answer – Drenai Mar 09 '21 at 14:04
  • Great solution that worked for me. Regarding the unsubscribe you could avoid this by wrapping the subscription in a service and publish the values using an rxjs subject. Your components can then subscribe to the subjects of the service (or observables created from them). That way only one subscription for the router events will be opened. – benjiman Nov 08 '22 at 22:30
13

An alternative (on Angular 7+) is to subscribe to the ActivatedRoute url observable and use "withLatestFrom" to get the latest paramsMap and queryParamsMap. It appears that the params are set before the url observable emits:

this.route.url.pipe(
  withLatestFrom(this.route.paramMap, this.route.queryParamMap)
).subscribe(([url, paramMap, queryParamMap]) => {
  // Do something with url, paramsMap and queryParamsMap
});

https://rxjs-dev.firebaseapp.com/api/operators/withLatestFrom

https://angular.io/api/router/ActivatedRoute

Nibor
  • 1,096
  • 8
  • 11
  • 1
    Thank you so much. Half of the people here don't understand the fundamental issue of sometimes needing queryParams and sometimes only params. – SleekPanther Jul 29 '20 at 20:08
7

Following Shifatul's answer, this supposes working for the observables changed at the same time and just fire once:

import { combineLatest } from 'rxjs';
import { map, debounceTime } from 'rxjs/operators';

...

combineLatest(this.route.params, this.route.queryParams)
    .pipe(map(results => ({params: results[0].xxx, query: results[1]})), debounceTime(0))
    .subscribe(results => {
        console.log(results);
    });
Vincent
  • 1,178
  • 1
  • 12
  • 25
5

Shifatul's answer extended for ...

  • Angular 9+ (using an array for combineLastest) and
  • simpler syntax (using typescript array parameter decomposition):
import { combineLatest } from 'rxjs';
import { map } from 'rxjs/operators';

// ...

combineLatest([this.route.params, this.route.queryParams])
    .subscribe(([params, queryParams]) => {
        console.log("your route data:", params, queryParams);
    });
Robert
  • 156
  • 1
  • 4
3

The proposed solutions using combineLatest do not work properly. After the first route change, you will get an event every time the param or queryParam value changes, which means you get calls in an intermediate state when one has changed but the other has not, very bad.

@rekna's solution pretty much works for me, except I wasn't getting an event when my app was first loaded, so here's my modification to @rekna's solution:

this.routeUpdate(this.activatedRoute.snapshot);
this.routeSubscription = this.router.events
  .pipe(filter(event => event instanceof NavigationEnd))
  .subscribe(event => this.routeUpdate(this.activatedRoute.snapshot));

Where routeUpdate is a method to do whatever parameter handling is required.

1

Use ActivatedRouteSnapshot from your ActivatedRoute

ActivatedRouteSnapshot interface has params and queryParams property, and you could get the both value at the same time.

constructor(private route: ActivatedRoute) {
    console.log(this.route.snapshot.params);
    console.log(this.route.snapshot.queryParams);
}

Edit : As OP stated, we only get the initial value of the parameters with this technique.

Example Plunker

Michael
  • 1,692
  • 1
  • 17
  • 19
1

I think you should use zip operator https://www.learnrxjs.io/operators/combination/zip.html

Because if you use combineLatest and change url params or query params you got value with new query params and old url params.

Urls for example:

http://localhost/1?value=10

http://localhost/2?value=20

http://localhost/3?value=30