debounced() creează o versiune „întârziată” a unui semnal: valoarea se actualizează abia după ce sursa a stat nemișcată un anumit timp. E cazul clasic al căsuței de căutare: nu vrei un request la fiecare tastă, ci doar după ce utilizatorul s-a oprit din scris.
debounced e marcat experimental (@experimental 22.0): API-ul se poate schimba. E sigur de folosit, dar ține-l izolat (de ex. într-un store sau într-o componentă de căutare), ca o eventuală schimbare să fie ușor de aplicat.
Exemplu: căutare cu debounce + httpResource
import { httpResource } from '@angular/common/http';
import { Component, debounced, signal } from '@angular/core';
@Component({
selector: 'app-user-search',
template: `
<input [value]="query()" (input)="query.set($any($event.target).value)" />
@if (search.isLoading()) { <span>aștept să termini de scris…</span> }
@for (u of users.value().items; track u.id) {
<p>{{ u.name }}</p>
}
`,
})
export class UserSearch {
readonly query = signal('');
// debounced() întoarce un Resource: valoarea se actualizează la 300 ms după ULTIMA schimbare.
readonly search = debounced(this.query, 300);
readonly users = httpResource<PageDto<UserDto>>(
() => `/api/users?q=${encodeURIComponent(this.search.value())}`,
{ defaultValue: { items: [], total: 0 } },
);
}∅∅- Ana Popescu · Cluj-Napoca
- Mihai Ionescu · București
- Ioana Dumitru · Iași
- Radu Stan · Timișoara
- Elena Marin · Brașov
- Andrei Georgescu · Cluj-Napoca
- Maria Constantin · Constanța
- Vlad Petrescu · Sibiu
- Cristina Pop · Oradea
- Bogdan Rusu · Iași
Debounce, throttle sau niciuna
Amândouă reduc numărul de cereri, în feluri diferite. Debounce așteaptă până face utilizatorul o pauză și trimite doar ultima valoare. Throttle trimite cel mult o dată la X milisecunde cât timp evenimentele continuă, așa că ecranul se actualizează și în timpul unei rafale lungi.
Fiecare punct e o cerere; numărul arată câte au fost. Treci cu mouse-ul peste un punct ca să vezi valoarea trimisă.
Pentru căutare în timp ce scrii, debounce e aproape mereu alegerea bună: nimeni n-are nevoie de rezultate pentru „ang” când e pe cale să scrie „angular”. Throttle se potrivește semnalelor continue, unde contează ultima stare în timpul mișcării: poziția de scroll, previzualizarea unui slider, redimensionarea, un cursor împărțit în timp real. Linia de debounce din laborator folosește chiar debounced(), cu o funcție de așteptare care citește slider-ul, deci o nouă durată se aplică de la tasta următoare.
import { debounced } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { fromEvent, map, throttleTime } from 'rxjs';
// debounce: aștepți o pauză, apoi ultima valoare (căutare în timp ce scrii)
readonly term = debounced(this.query, 300);
// throttle: cel mult o dată pe fereastră, cu prima și ultima valoare (scroll, resize)
readonly scrollY = toSignal(
fromEvent(window, 'scroll').pipe(
throttleTime(100, undefined, { leading: true, trailing: true }),
map(() => window.scrollY),
),
{ initialValue: 0 },
);API
Rezultatul nu e un Signal simplu, ci un Resource: ai value(), dar și isLoading() — util ca să arăți „aștept să termini de scris”.
const d = debounced(source, wait, options?);
// source: orice funcție reactivă — un signal, un computed, un input()
// wait: număr de ms SAU (value, lastSnapshot) => Promise<void> | void
// options: { injector?, equal? }
// rezultat: Resource<T> → d.value(), d.status(), d.isLoading()
// Exemplu: fără întârziere când câmpul e golit, 400 ms altfel
const term = debounced(this.query, (value) =>
value === '' ? undefined : new Promise<void>((resolve) => setTimeout(resolve, 400)),
);Înainte și acum
// Acum (v22, experimental)
readonly query = signal('');
readonly debouncedQuery = debounced(this.query, 300); // .value()// Înainte (RxJS): trei conversii pentru un singur debounce
readonly query = signal('');
readonly debouncedQuery = toSignal(
toObservable(this.query).pipe(debounceTime(300), distinctUntilChanged()),
{ initialValue: '' },
);Varianta RxJS nu e greșită — dacă ai deja un flux RxJS (cu switchMap, retry, etc.) rămâi pe el. Dar pentru „vreau doar o valoare întârziată”, debounced e mult mai simplu.
În formulare: debounce() din Signal Forms
Pentru câmpurile unui formular semnal nu ai nevoie de debounced(): există regula debounce() în schemă, iar validatorii async au și ei opțiune de debounce (v22). Vezi Signal Forms — avansat.
// În Signal Forms există regula dedicată debounce() pentru câmpuri:
import { debounce, form, validateHttp } from '@angular/forms/signals';
readonly signup = form(this.model, (path) => {
debounce(path.username, 300); // modelul se actualizează la 300 ms după tastare
validateHttp(path.username, {
request: ({ value }) => `/api/username-available?username=${value()}`,
onSuccess: (res: { available: boolean }) =>
res.available ? undefined : { kind: 'taken', message: 'Username ocupat' },
onError: () => ({ kind: 'network', message: 'Nu am putut verifica' }),
});
});Când DA, când NU
- Căutare / autocomplete.
- Filtre care declanșează request-uri (slider de preț, input numeric).
- Salvare automată (autosave) după ce utilizatorul se oprește din scris.
- Calcule scumpe derivate din input text.
- Câmpuri de formular semnal → regula
debounce(). - Throttle (maxim o dată la X ms, ex. scroll) → RxJS
throttleTimesaurequestAnimationFrame. - Fluxuri cu operatori multipli → rămâi în RxJS.