Angular Signals - debounce in effect()

1.8k Views Asked by At

I have a signal that is bound to an input field. I'm trying to define an effect() for the searchTerm, but because it's user input, I'd like to debounce that effect (i.e. rxjs) so that the search doesn't happen with each keystroke. I'm unclear on how to accomplish this and the documentation doesn't really cover the situation.

<input [ngModel]="searchTerm()" (ngModelChange)="searchTerm.set($event)">
effect(() => {
    if (this.searchTerm() !== '') { this.search(); }
});
3

There are 3 best solutions below

3
On BEST ANSWER

There are no built-in solution for debounce in Signal. However, you can create a custom function to do that:

function debouncedSignal<T>(input: Signal<T>, timeOutMs = 0): Signal<T> {
  const debounceSignal = signal(input());
  effect(() => {
    const value = input();
    const timeout = setTimeout(() => {
      debounceSignal.set(value);
    }, timeOutMs);
    return () => {
      clearTimeout(timeout);
    };
  });
  return debounceSignal;
}

const itemsList = [
  { name: 'Product A', category: 'Category 1' },
  { name: 'Product B', category: 'Category 2' },
  { name: 'Product C', category: 'Category 1' },
];

@Component({
  selector: 'my-app',
  standalone: true,
  imports: [CommonModule, FormsModule],
  template: `
    <input [ngModel]="searchTerm()" (ngModelChange)="searchTerm.set($any($event))">
    <ul>
      <li *ngFor="let item of items">
        {{item.name}}
      </li>
    </ul>
  `,
})
export class App {
  items = itemsList;

  searchTerm = signal('');

  debounceSearchValue = debouncedSignal(this.searchTerm, 500);

  constructor() {
    effect(() => {
      this.search(this.debounceSearchValue());
    });
  }

  private search(value: string): void {
    if (!value) {
      this.items = itemsList;
    }

    const query = value.toLowerCase();

    this.items = itemsList.filter(
      (item) =>
        item.name.toLowerCase().includes(query) ||
        item.category.toLowerCase().includes(query)
    );
  }
}

This solution is the way so complicate, so I recommend to use RxJS for cleaner and more efficient code

1
On

Something like

searchFor = toSignal(toObservable(this.searchTerm).pipe(debounceTime(100)), {
    initialValue: '',
});
0
On

Debounced signals can be achieved using the following utility function. This is a modified version of the idea of @An Nguyen, which contained two bugs.

One of the real power of signals lies in the fact that it is easy to create standalone functions that create a signal (or effect). This allows for easy sharing, and for nicely applying "composition over inheritance".

export function debouncedSignal<T>(
  sourceSignal: Signal<T>,
  debounceTimeInMs = 0
): Signal<T> {
  const debounceSignal = signal(sourceSignal());
  effect(
    (onCleanup) => {
      const value = sourceSignal();
      const timeout = setTimeout(
        () => debounceSignal.set(value),
        debounceTimeInMs
      );

      // The `onCleanup` argument is a function which is called when the effect
      // runs again (and when it is destroyed).
      // By clearing the timeout here we achieve proper debouncing.
      // See https://angular.io/guide/signals#effect-cleanup-functions
      onCleanup(() => clearTimeout(timeout));
    },
    { allowSignalWrites: true }
  );
  return debounceSignal;
}

Usage:

debouncedSearchTerm = debouncedSignal(this.searchTerm, 250);