debounced() crée une version « retardée » d’un signal : la valeur ne se met à jour qu’après que la source est restée immobile un moment. C’est le cas classique de la zone de recherche : vous ne voulez pas une requête à chaque frappe, seulement quand l’utilisateur a arrêté de taper.
debounced est marqué expérimental (@experimental 22.0) : l’API peut changer. Vous pouvez l’utiliser sans risque, mais gardez-le isolé (par exemple dans un Store ou un composant de recherche), pour qu’un éventuel changement soit facile à appliquer.
Exemple : recherche avec 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>en attente de la fin de votre saisie…</span> }
@for (u of users.value().items; track u.id) {
<p>{{ u.name }}</p>
}
`,
})
export class UserSearch {
readonly query = signal('');
// debounced() renvoie une Resource : la valeur est mise à jour 300 ms après le DERNIER changement.
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, ou aucun des deux
Les deux réduisent le nombre de requêtes, de façons différentes. Debounce attend que l’utilisateur fasse une pause et n’envoie que la dernière valeur. Throttle envoie au plus une fois toutes les X millisecondes tant que les événements continuent, donc l’écran se met à jour même pendant une longue rafale.
Chaque point est une requête ; le nombre indique combien il y en a eu. Survolez un point pour voir la valeur envoyée.
Pour une recherche pendant la frappe, debounce est presque toujours le bon choix : personne n’a besoin de résultats pour « ang » quand il s’apprête à taper « angular ». Throttle convient aux signaux continus où le dernier état compte pendant le mouvement : la position de défilement, l’aperçu d’un curseur, le redimensionnement, un curseur partagé en temps réel. La ligne debounce du labo utilise le vrai debounced(), avec une fonction d’attente qui lit le curseur : une nouvelle durée s’applique dès la frappe suivante.
import { debounced } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { fromEvent, map, throttleTime } from 'rxjs';
// debounce : attendre une pause, puis la dernière valeur (recherche pendant la frappe)
readonly term = debounced(this.query, 300);
// throttle : au plus une fois par fenêtre, première et dernière valeur comprises (scroll, resize)
readonly scrollY = toSignal(
fromEvent(window, 'scroll').pipe(
throttleTime(100, undefined, { leading: true, trailing: true }),
map(() => window.scrollY),
),
{ initialValue: 0 },
);API
Le résultat n’est pas un simple Signal mais une Resource : vous obtenez value(), et aussi isLoading() — utile pour afficher « j’attends que vous ayez fini de taper ».
const d = debounced(source, wait, options?);
// source : toute fonction réactive — un signal, un computed, un input()
// attente : un nombre de ms OU (value, lastSnapshot) => Promise<void> | void
// options: { injector?, equal? }
// résultat : Resource<T> → d.value(), d.status(), d.isLoading()
// Exemple : aucun délai quand le champ est vidé, 400 ms sinon
const term = debounced(this.query, (value) =>
value === '' ? undefined : new Promise<void>((resolve) => setTimeout(resolve, 400)),
);Avant et maintenant
// Maintenant (v22, expérimental)
readonly query = signal('');
readonly debouncedQuery = debounced(this.query, 300); // .value()// Avant (RxJS) : trois conversions pour un seul debounce
readonly query = signal('');
readonly debouncedQuery = toSignal(
toObservable(this.query).pipe(debounceTime(300), distinctUntilChanged()),
{ initialValue: '' },
);La version RxJS n’est pas mauvaise — si vous avez déjà un flux RxJS (avec switchMap, retry, etc.), restez dessus. Mais pour « je veux juste une valeur retardée », debounced est bien plus simple.
Dans les formulaires : debounce() de Signal Forms
Pour les champs d’un Signal Form, vous n’avez pas besoin de debounced() : il existe la règle debounce() dans le schéma, et les validateurs asynchrones ont aussi une option de debounce (v22). Voir Signal Forms — avancé.
// Signal Forms a une règle debounce() dédiée aux champs :
import { debounce, form, validateHttp } from '@angular/forms/signals';
readonly signup = form(this.model, (path) => {
debounce(path.username, 300); // le modèle est mis à jour 300 ms après la frappe
validateHttp(path.username, {
request: ({ value }) => `/api/username-available?username=${value()}`,
onSuccess: (res: { available: boolean }) =>
res.available ? undefined : { kind: 'taken', message: 'Nom d’utilisateur déjà pris' },
onError: () => ({ kind: 'network', message: 'Vérification impossible' }),
});
});Quand OUI, quand NON
- Recherche / autocomplétion.
- Filtres qui déclenchent des requêtes (un curseur de prix, un champ numérique).
- Enregistrement automatique après que l’utilisateur a arrêté de taper.
- Calculs coûteux dérivés d’une saisie de texte.
- Champs de Signal Form → la règle
debounce(). - Throttle (au plus une fois toutes les X ms, p. ex. le scroll) →
throttleTimede RxJS ourequestAnimationFrame. - Flux avec plusieurs opérateurs → restez sur RxJS.