skip to content

In Angular, how do you move a product list to page 3 without dropping its active filter query parameters, and what does queryParamsHandling control?

level: middleimportance: should knowfreq 55%

answer

  1. replace is the default
  2. merge versus preserve
  3. null deletes a key
  4. empty commands keep the path

basics

~10 s

Navigate with empty commands, queryParams {page: 3} and queryParamsHandling 'merge' so current filters are kept. The option decides how new query params combine with current ones: 'replace' (default), 'merge' or 'preserve'.

solid answer

~40 s

`queryParamsHandling` is a URL-creation option on `Router.navigate()`, `createUrlTree()` and `routerLink`. With the default, `'replace'` (or the legacy `''`), the new `queryParams` replace the whole query string, so `{page: 3}` drops `category` and `sort`. `'merge'` spreads the new values over the current ones, and `'preserve'` keeps the current query and **ignores** the `queryParams` you passed. In a merge, a `null` or `undefined` value removes that key, which is how you reset `page` when a filter changes. Empty commands keep the current path: `router.navigate([], {relativeTo: this.route, queryParams: {page: 3}, queryParamsHandling: 'merge'})`. The fragment is dropped unless you pass `preserveFragment`, and `withRouterConfig({defaultQueryParamsHandling: 'merge'})` changes the app-wide default.

code

ts · 24 lines
ts
import {Component, inject} from '@angular/core';
import {ActivatedRoute, Router, RouterLink} from '@angular/router';

@Component({
  selector: 'app-product-pager',
  imports: [RouterLink],
  template: `
    <a [routerLink]="[]" [queryParams]="{page: 3}" queryParamsHandling="merge">Page 3</a>
    <button (click)="setCategory('games')">Games</button>
  `,
})
export class ProductPager {
  private readonly router = inject(Router);
  private readonly route = inject(ActivatedRoute);

  setCategory(category: string): void {
    // keeps sort, switches category, removes page
    this.router.navigate([], {
      relativeTo: this.route,
      queryParams: {category, page: null},
      queryParamsHandling: 'merge',
    });
  }
}

go deeper

for a junior

Know that query params go in the queryParams option or input, not in the path string, and that by default they replace the existing query.

for a middle

Explain replace, merge and preserve, empty commands to keep the path, null to delete a key, and that the fragment needs preserveFragment.

for a senior

Design the pager, filter and clear-all flows so paging resets correctly and history stays usable, using replaceUrl for keystroke updates.

for a principal

Weigh an app-wide defaultQueryParamsHandling against explicit per-call handling, knowing a global default changes every existing link at once.

## The problem A product list keeps its state in the URL: `/products?category=books&sort=price&page=2`. The pager must change only `page`; the filter panel must change `category` and reset `page`. Angular's router offers `queryParamsHandling` for exactly this, and its default does the opposite of what a pager needs. ## The four values `QueryParamsHandling` is typed `'merge' | 'preserve' | 'replace' | ''` and is read by `Router.createUrlTree()`, which both `Router.navigate()` and the `RouterLink` directive use: | Value | Resulting query | Typical use | |---|---|---| | `'replace'` (default) | only the `queryParams` you pass | a link to a fresh screen | | `''` | same as `'replace'`, kept for legacy reasons | older code | | `'merge'` | current params, then yours on top | pager, sort toggle, one filter | | `'preserve'` | current params unchanged; yours ignored | moving to another path while keeping the query | Two details are easy to miss: - **`'preserve'` discards the `queryParams` you pass.** It does not mean "preserve and add"; that is `'merge'`. - **In a merge, `null` or `undefined` deletes a key.** The router strips null and undefined values after combining, so `{category: 'games', page: null}` switches the filter and removes `page` so the list restarts at page one. ## Keeping the path while changing the query If the commands array is empty, `createUrlTree()` keeps the current path and replaces only the query and fragment. That makes the pager call short: ```ts this.router.navigate([], { relativeTo: this.route, queryParams: {page: 3}, queryParamsHandling: 'merge', }); ``` The template equivalent is `<a [routerLink]="[]" [queryParams]="{page: 3}" queryParamsHandling="merge">3</a>`, which also renders a real `href`, so users can open page 3 in a new tab. ## The fragment does not survive by default `fragment` is also a creation option: if you pass none, the new URL has none. A list at `/products?page=2#reviews` loses `#reviews` on a merge unless you set `preserveFragment: true`, or `preserveFragment` on the link, or pass the fragment explicitly. ## Changing the default Since Angular 18.2, `withRouterConfig({defaultQueryParamsHandling: 'merge'})` inside `provideRouter()` changes the fallback for every `createUrlTree()` call that does not state its own `queryParamsHandling`, including every `routerLink`. It is convenient for filter-heavy apps, but it silently changes every link that used to reset the query, so audit links to unrelated screens before turning it on. It has no effect on `navigateByUrl()`, which never builds a URL from options. ## History entries for rapid updates A search box that writes each keystroke into the URL would push one history entry per keystroke. Passing `replaceUrl: true` on those navigations replaces the current entry instead, so Back leaves the list instead of undoing typing. Keep pager clicks as normal navigations if Back should step through pages. ## The same rules on `routerLink` `RouterLink` passes its `queryParams`, `queryParamsHandling`, `fragment` and `preserveFragment` inputs to the same `createUrlTree()` call, so everything above applies to template links. One difference is reactivity: when a link uses `'merge'` or `'preserve'`, its rendered `href` depends on the current query, so the directive recomputes it after each navigation. A pager built from links therefore always shows the right `href` for page 3 with today's filters, which also makes it crawlable and shareable. ## Checklist for a URL-driven list 1. Pager and sort: empty commands, `relativeTo`, `'merge'`. 2. Filter change: `'merge'` with `page: null` to reset paging. 3. "Clear all": the default `'replace'` with `queryParams: {}` or no `queryParams`. 4. Typing: add `replaceUrl: true`. 5. Read the params back from the route reactively so the list follows Back and Forward.

  • How do you remove a single query parameter while keeping the others?
    Navigate with queryParamsHandling 'merge' and set that key to null or undefined. The router combines current and new params and then strips null and undefined values, so the key disappears while the rest stay.
  • Why is setting defaultQueryParamsHandling to 'merge' app-wide risky?
    Every routerLink and navigate() call without an explicit queryParamsHandling starts carrying the current query along, so links to unrelated screens leak filters such as category into places that never expected them. Audit those links, or set 'replace' on them explicitly.

saying these in an interview costs you the question

  • Expecting 'preserve' to keep current params and also add the new ones
  • Believing new queryParams are merged into the current query by default
  • Resetting a param by setting it to an empty string instead of null
  • Assuming the URL fragment survives a query-param-only navigation
  • Expecting queryParamsHandling to work with navigateByUrl