typescript 57 lines · 8 steps

Debounced search that syncs to the URL in Angular

A standalone Angular component turns keystrokes into debounced signals and mirrors the search term in the query string.

Explained by highlit
1import { Component, inject, effect } from '@angular/core';
2import { FormControl, ReactiveFormsModule } from '@angular/forms';
3import { ActivatedRoute, Router } from '@angular/router';
4import { toSignal } from '@angular/core/rxjs-interop';
5import { debounceTime, distinctUntilChanged, map, startWith } from 'rxjs';
6 
7@Component({
8 selector: 'app-product-search',
9 standalone: true,
10 imports: [ReactiveFormsModule],
11 template: `
12 <input
13 type="search"
14 placeholder="Search products"
15 [formControl]="query"
16 />
17 @if (activeQuery()) {
18 <p>Showing results for “{{ activeQuery() }}”</p>
19 }
20 `,
21})
22export class ProductSearchComponent {
23 private readonly route = inject(ActivatedRoute);
24 private readonly router = inject(Router);
25 
26 protected readonly query = new FormControl('', { nonNullable: true });
27 
28 protected readonly activeQuery = toSignal(
29 this.route.queryParamMap.pipe(map((params) => params.get('q') ?? '')),
30 { initialValue: '' },
31 );
32 
33 private readonly debouncedInput = toSignal(
34 this.query.valueChanges.pipe(
35 debounceTime(300),
36 map((value) => value.trim()),
37 distinctUntilChanged(),
38 startWith(''),
39 ),
40 { initialValue: '' },
41 );
42 
43 constructor() {
44 this.query.setValue(this.activeQuery(), { emitEvent: false });
45 
46 effect(() => {
47 const term = this.debouncedInput();
48 if (term === this.activeQuery()) return;
49 
50 this.router.navigate([], {
51 relativeTo: this.route,
52 queryParams: { q: term || null, page: null },
53 queryParamsHandling: 'merge',
54 });
55 });
56 }
57}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Bridging RxJS streams to signals with toSignal lets template and effects read reactive state synchronously.
  2. 2Debouncing input before navigation avoids flooding the router with intermediate keystrokes.
  3. 3Guarding an effect against its own output prevents feedback loops when state flows both ways.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Debounced search that syncs to the URL in Angular — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code