Query Params & Fragments
Query parameters and fragments carry optional, non-hierarchical state in the URL — things like search filters, pagination, sort order, or a scroll-to anchor. Unlike route parameters, they are not part of the route’s path definition, so any route can read and write them freely. Getting them right means your application’s state survives a page reload, a shared link, or a browser back button, which is exactly what users expect.
Query params vs fragments vs route params
It helps to be precise about which part of the URL you are touching. In https://app.dev/products?category=books&page=2#reviews, the three pieces play different roles.
| Part | Example | Purpose | Read with |
|---|---|---|---|
| Route parameter | /products/42 | Required, identifies a resource | route.paramMap |
| Query parameters | ?category=books&page=2 | Optional, modifies the view (filters, paging) | route.queryParamMap |
| Fragment | #reviews | Anchor / client-side scroll target | route.fragment |
Query params are loosely coupled to a route — you can add ?page=2 to almost any URL without declaring it in your route config.
Setting query params and fragments on navigation
Both RouterLink and the imperative Router.navigate accept queryParams and fragment options. Use the link form in templates and the imperative form in component logic.
<a
routerLink="/products"
[queryParams]="{ category: 'books', page: 2 }"
fragment="reviews"
>
Books, page 2
</a>
import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';
@Component({
selector: 'app-search-box',
standalone: true,
template: `<button (click)="search('phones')">Search phones</button>`,
})
export class SearchBoxComponent {
private router = inject(Router);
search(term: string): void {
this.router.navigate(['/products'], {
queryParams: { q: term, page: 1 },
fragment: 'results',
});
}
}
The resulting URL is /products?q=phones&page=1#results.
Reading query params and fragments
Inject ActivatedRoute and subscribe to the relevant observable. The queryParamMap and fragment observables emit again whenever the URL changes without re-creating the component, which is the common case when only the query string changes.
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { toSignal } from '@angular/core/rxjs-interop';
import { map } from 'rxjs';
@Component({
selector: 'app-product-list',
standalone: true,
template: `
<h2>Category: {{ category() ?? 'all' }}</h2>
<p>Page {{ page() }}</p>
`,
})
export class ProductListComponent {
private route = inject(ActivatedRoute);
// Convert the queryParamMap stream into signals for templates.
category = toSignal(
this.route.queryParamMap.pipe(map((p) => p.get('category'))),
{ initialValue: null },
);
page = toSignal(
this.route.queryParamMap.pipe(map((p) => Number(p.get('page') ?? 1))),
{ initialValue: 1 },
);
}
For a one-time read where you do not need to react to later changes, route.snapshot.queryParamMap.get('page') is fine. Prefer the observable/signal form whenever the same component stays mounted across query changes.
Query param values are always strings (or
null). Cast them explicitly —Number(...),=== 'true', etc. — and never trust the type. Arrays arrive as repeated keys:?tag=a&tag=bis read withqueryParamMap.getAll('tag').
Angular’s binding-based component inputs (withComponentInputBinding()) can also map query params straight to @Input() properties, removing the boilerplate above:
provideRouter(routes, withComponentInputBinding());
import { Input } from '@angular/core';
export class ProductListComponent {
@Input() category?: string; // bound from ?category=
@Input() page = '1'; // bound from ?page=
}
Preserving params across navigations
By default, navigating away drops the current query string and fragment. The queryParamsHandling strategy controls this.
| Value | Effect |
|---|---|
'' (default) | Replace — drop existing query params |
'merge' | Merge new params into existing ones |
'preserve' | Keep existing params, ignore new ones |
// Toggle one filter while keeping every other query param intact.
this.router.navigate([], {
relativeTo: this.route,
queryParams: { page: 3 },
queryParamsHandling: 'merge',
});
Passing { sort: null } with 'merge' removes a single param. To keep the fragment across a navigation, set preserveFragment: true.
<a routerLink="/products" queryParamsHandling="merge" [preserveFragment]="true">
Refine
</a>
Fragments and scrolling
A fragment is most useful as a scroll anchor. Enable automatic anchor scrolling once at bootstrap, and the router will scroll to the element whose id matches the fragment after navigation.
import { provideRouter, withInMemoryScrolling } from '@angular/router';
provideRouter(routes,
withInMemoryScrolling({ anchorScrolling: 'enabled', scrollPositionRestoration: 'enabled' }),
);
<section id="reviews">
<h2>Customer reviews</h2>
</section>
Now navigating to /products#reviews scrolls straight to that section. For manual control, read route.fragment and call scrollIntoView() yourself.
this.route.fragment.subscribe((id) => {
if (id) document.getElementById(id)?.scrollIntoView({ behavior: 'smooth' });
});
Output:
Navigated to /products?category=books&page=2#reviews
Page scrolled to element #reviews
A practical filter example
Treating the URL as the single source of truth for filter state means the back button, bookmarks, and shared links all “just work.”
applyFilter(category: string): void {
this.router.navigate([], {
relativeTo: this.route,
queryParams: { category, page: 1 }, // reset paging on new filter
queryParamsHandling: 'merge',
});
}
The component re-reads queryParamMap, recomputes the product list, and the URL reflects exactly what the user sees.
Best practices
- Treat the URL (query params + fragment) as the source of truth for view state like filters, search, and pagination — never duplicate it in component state.
- Always coerce query param values; they are strings or
null, never numbers or booleans. - Use
queryParamsHandling: 'merge'when changing one filter so you do not clobber the others, and passnullto remove a param. - Prefer
queryParamMap/fragmentobservables (orwithComponentInputBinding()) oversnapshotwhen the component survives query changes. - Enable
withInMemoryScrolling({ anchorScrolling: 'enabled' })instead of hand-rolling fragment scroll logic. - Keep query params flat and short — they are user-visible and length-limited; store large state elsewhere.