Getting Started

Angular Quick Start

TanStack Pacer controls when your functions run. The Angular adapter, @tanstack/angular-pacer, wraps each Pacer utility in an inject* function. Call it in an injection context, such as a component field initializer. The utility cleans up when the component is destroyed and exposes the state you select as a signal.

This page starts with a debounced search input, then covers the patterns most apps need next.

Installation

sh
npm install @tanstack/angular-pacer
npm install @tanstack/angular-pacer

The adapter re-exports everything from @tanstack/pacer, so you do not need to install the core package. See Installation for other package managers.

Your first debouncer

The component below is complete. The input updates on every keystroke. debouncedQuery() updates 500 ms after the user stops typing.

ts
import { Component, signal } from '@angular/core'
import { injectDebouncedValue } from '@tanstack/angular-pacer'

@Component({
  selector: 'app-search',
  template: `
    <input
      [value]="query()"
      (input)="query.set($any($event.target).value)"
      placeholder="Search..."
    />
    <p>Searching for: {{ debouncedQuery() }}</p>
  `,
})
export class SearchComponent {
  readonly query = signal('')
  readonly debouncedQuery = injectDebouncedValue(this.query, { wait: 500 })
}
import { Component, signal } from '@angular/core'
import { injectDebouncedValue } from '@tanstack/angular-pacer'

@Component({
  selector: 'app-search',
  template: `
    <input
      [value]="query()"
      (input)="query.set($any($event.target).value)"
      placeholder="Search..."
    />
    <p>Searching for: {{ debouncedQuery() }}</p>
  `,
})
export class SearchComponent {
  readonly query = signal('')
  readonly debouncedQuery = injectDebouncedValue(this.query, { wait: 500 })
}

Pass debouncedQuery to your data fetching instead of query, and a fast typist sends one request instead of one per keystroke.

Pick a function

Every utility comes in several shapes. They share one engine and differ in what they hand back to you. For debouncing:

FunctionReturnsUse it when
injectDebouncedValueA signal of the debounced value, with a debouncer propertyYou already have a signal and want a lagging copy
injectDebouncedSignalA signal with a debounced set and a debouncer propertyYou want signal() with a debounced setter
injectDebouncerThe debouncer instanceYou need maybeExecute, flush, cancel, or reactive state

The other utilities follow the same naming pattern:

UtilityInstance functionAsync instance functionGuide
DebouncinginjectDebouncerinjectAsyncDebouncerDebouncing
ThrottlinginjectThrottlerinjectAsyncThrottlerThrottling
Rate limitinginjectRateLimiterinjectAsyncRateLimiterRate Limiting
QueuinginjectQueuerinjectAsyncQueuerQueuing
BatchinginjectBatcherinjectAsyncBatcherBatching

Not sure which utility you need? Read Which Pacer Utility Should I Choose?.

Common patterns

Control the debouncer directly

injectDebouncer returns the instance. Call maybeExecute from your event handler, and use flush or cancel when the user acts before the timer fires.

ts
import { Component, signal } from '@angular/core'
import { injectDebouncer } from '@tanstack/angular-pacer'

@Component({
  selector: 'app-draft-editor',
  template: `
    <textarea [value]="draft()" (input)="onInput($event)"></textarea>
    <button (click)="saver.flush()">Save now</button>
    <button (click)="saver.cancel()">Discard</button>
    @if (saver.state().isPending) {
      <p>Unsaved changes...</p>
    }
  `,
})
export class DraftEditorComponent {
  readonly draft = signal('')

  readonly saver = injectDebouncer(
    (text: string) => saveDraft(text),
    { wait: 1000 },
    (state) => ({ isPending: state.isPending }),
  )

  onInput(event: Event) {
    const value = (event.target as HTMLTextAreaElement).value
    this.draft.set(value)
    this.saver.maybeExecute(value)
  }
}
import { Component, signal } from '@angular/core'
import { injectDebouncer } from '@tanstack/angular-pacer'

@Component({
  selector: 'app-draft-editor',
  template: `
    <textarea [value]="draft()" (input)="onInput($event)"></textarea>
    <button (click)="saver.flush()">Save now</button>
    <button (click)="saver.cancel()">Discard</button>
    @if (saver.state().isPending) {
      <p>Unsaved changes...</p>
    }
  `,
})
export class DraftEditorComponent {
  readonly draft = signal('')

  readonly saver = injectDebouncer(
    (text: string) => saveDraft(text),
    { wait: 1000 },
    (state) => ({ isPending: state.isPending }),
  )

  onInput(event: Event) {
    const value = (event.target as HTMLTextAreaElement).value
    this.draft.set(value)
    this.saver.maybeExecute(value)
  }
}

Select the state you render

The third argument is a selector. saver.state is a signal, so call saver.state() to read it. Without a selector, it returns {} and never changes. Select only the fields your template reads, so other state changes do not trigger change detection.

Each utility's guide lists the state fields it exposes.

Make options reactive

A plain options object is read once. To make an option follow a signal or an input, pass a factory that returns the options:

ts
import { Component, input } from '@angular/core'
import { injectDebouncer } from '@tanstack/angular-pacer'

@Component({ selector: 'app-search', template: `...` })
export class SearchComponent {
  readonly wait = input.required<number>()

  readonly searcher = injectDebouncer(
    (query: string) => this.fetchResults(query),
    () => ({ wait: this.wait() }),
  )
}
import { Component, input } from '@angular/core'
import { injectDebouncer } from '@tanstack/angular-pacer'

@Component({ selector: 'app-search', template: `...` })
export class SearchComponent {
  readonly wait = input.required<number>()

  readonly searcher = injectDebouncer(
    (query: string) => this.fetchResults(query),
    () => ({ wait: this.wait() }),
  )
}

Property getters such as get wait() { return this.wait() } work too. Writing { wait: this.wait() } without the factory or getter reads the signal once and never updates.

With a factory or getters, the adapter waits to create the utility until Angular binds the component inputs. When a signal changes, the adapter updates the same utility. Pending work and state survive. A changed wait applies to the next call. It does not reschedule a timer that is already running. Setting enabled to false cancels pending work. key, initialState, and initialItems apply only when the utility is created.

Run async work

The async functions await your function, track execution state, and report errors. This search debounces the request and shows a loading state:

ts
import { Component, signal } from '@angular/core'
import { injectAsyncDebouncer } from '@tanstack/angular-pacer'

@Component({
  selector: 'app-async-search',
  template: `
    <input (input)="searcher.maybeExecute($any($event.target).value)" />
    @if (searcher.state().isExecuting) {
      <p>Loading...</p>
    }
    <ul>
      @for (result of results(); track result.id) {
        <li>{{ result.title }}</li>
      }
    </ul>
  `,
})
export class AsyncSearchComponent {
  readonly results = signal<Array<SearchResult>>([])

  readonly searcher = injectAsyncDebouncer(
    async (term: string) => {
      const data = await fetchSearchResults(term)
      this.results.set(data)
      return data
    },
    {
      wait: 300,
      onError: (error) => console.error('Search failed:', error),
    },
    (state) => ({ isExecuting: state.isExecuting }),
  )
}
import { Component, signal } from '@angular/core'
import { injectAsyncDebouncer } from '@tanstack/angular-pacer'

@Component({
  selector: 'app-async-search',
  template: `
    <input (input)="searcher.maybeExecute($any($event.target).value)" />
    @if (searcher.state().isExecuting) {
      <p>Loading...</p>
    }
    <ul>
      @for (result of results(); track result.id) {
        <li>{{ result.title }}</li>
      }
    </ul>
  `,
})
export class AsyncSearchComponent {
  readonly results = signal<Array<SearchResult>>([])

  readonly searcher = injectAsyncDebouncer(
    async (term: string) => {
      const data = await fetchSearchResults(term)
      this.results.set(data)
      return data
    },
    {
      wait: 300,
      onError: (error) => console.error('Search failed:', error),
    },
    (state) => ({ isExecuting: state.isExecuting }),
  )
}

maybeExecute returns a promise that resolves with your function's result. The async utilities also support retries and cancellation through an AbortSignal. See the Async Debouncing Guide.

Limit how often an action runs

A rate limiter allows a fixed number of calls per window and rejects the rest:

ts
import { Component } from '@angular/core'
import { injectRateLimiter } from '@tanstack/angular-pacer'

@Component({
  selector: 'app-send-button',
  template: `
    <button (click)="limiter.maybeExecute('Hello')">Send</button>
    <p>Rejected: {{ limiter.state().rejectionCount }}</p>
  `,
})
export class SendButtonComponent {
  readonly limiter = injectRateLimiter(
    (message: string) => sendMessage(message),
    {
      limit: 5,
      window: 60_000,
      onReject: (limiter) =>
        alert(`Slow down. Try again in ${limiter.getMsUntilNextWindow()} ms.`),
    },
    (state) => ({ rejectionCount: state.rejectionCount }),
  )
}
import { Component } from '@angular/core'
import { injectRateLimiter } from '@tanstack/angular-pacer'

@Component({
  selector: 'app-send-button',
  template: `
    <button (click)="limiter.maybeExecute('Hello')">Send</button>
    <p>Rejected: {{ limiter.state().rejectionCount }}</p>
  `,
})
export class SendButtonComponent {
  readonly limiter = injectRateLimiter(
    (message: string) => sendMessage(message),
    {
      limit: 5,
      window: 60_000,
      onReject: (limiter) =>
        alert(`Slow down. Try again in ${limiter.getMsUntilNextWindow()} ms.`),
    },
    (state) => ({ rejectionCount: state.rejectionCount }),
  )
}

Process items in order

A queuer keeps every item and processes them in order. With injectAsyncQueuer, concurrency sets how many run at once:

ts
import { Component } from '@angular/core'
import { injectAsyncQueuer } from '@tanstack/angular-pacer'

@Component({
  selector: 'app-uploader',
  template: `
    <input type="file" multiple (change)="onFiles($event)" />
    <p>
      Uploading {{ queue.state().activeItems.length }}, waiting
      {{ queue.state().size }}
    </p>
  `,
})
export class UploaderComponent {
  readonly queue = injectAsyncQueuer(
    async (file: File) => uploadFile(file),
    { concurrency: 3 },
    (state) => ({ size: state.size, activeItems: state.activeItems }),
  )

  onFiles(event: Event) {
    const files = (event.target as HTMLInputElement).files
    for (const file of files ?? []) this.queue.addItem(file)
  }
}
import { Component } from '@angular/core'
import { injectAsyncQueuer } from '@tanstack/angular-pacer'

@Component({
  selector: 'app-uploader',
  template: `
    <input type="file" multiple (change)="onFiles($event)" />
    <p>
      Uploading {{ queue.state().activeItems.length }}, waiting
      {{ queue.state().size }}
    </p>
  `,
})
export class UploaderComponent {
  readonly queue = injectAsyncQueuer(
    async (file: File) => uploadFile(file),
    { concurrency: 3 },
    (state) => ({ size: state.size, activeItems: state.activeItems }),
  )

  onFiles(event: Event) {
    const files = (event.target as HTMLInputElement).files
    for (const file of files ?? []) this.queue.addItem(file)
  }
}

Set default options

providePacerOptions sets default options for every Pacer utility in the injector tree where you provide it. Options passed to an inject* function override the defaults.

ts
import { providePacerOptions } from '@tanstack/angular-pacer'
import type { ApplicationConfig } from '@angular/core'

export const appConfig: ApplicationConfig = {
  providers: [
    providePacerOptions({
      debouncer: { wait: 500 },
      asyncQueuer: { concurrency: 3 },
      rateLimiter: { limit: 5, window: 60_000 },
    }),
  ],
}
import { providePacerOptions } from '@tanstack/angular-pacer'
import type { ApplicationConfig } from '@angular/core'

export const appConfig: ApplicationConfig = {
  providers: [
    providePacerOptions({
      debouncer: { wait: 500 },
      asyncQueuer: { concurrency: 3 },
      rateLimiter: { limit: 5, window: 60_000 },
    }),
  ],
}

To share options between specific utilities instead, define them once with an option helper. Helpers such as debouncerOptions return the object you pass in, typed for that utility:

ts
import { debouncerOptions, injectDebouncer } from '@tanstack/angular-pacer'

const searchOptions = debouncerOptions({ wait: 500, leading: false })

// In a component field
readonly debouncer = injectDebouncer(search, { ...searchOptions, key: 'search' })
import { debouncerOptions, injectDebouncer } from '@tanstack/angular-pacer'

const searchOptions = debouncerOptions({ wait: 500, leading: false })

// In a component field
readonly debouncer = injectDebouncer(search, { ...searchOptions, key: 'search' })

Control cleanup

When the component is destroyed, debouncers, throttlers, and batchers cancel pending work, and queuers stop processing. Async utilities also abort active work. To keep work instead, pass onUnmount. It replaces the default cleanup:

ts
readonly saver = injectDebouncer(saveDraft, {
  wait: 1000,
  onUnmount: (debouncer) => debouncer.flush(),
})
readonly saver = injectDebouncer(saveDraft, {
  wait: 1000,
  onUnmount: (debouncer) => debouncer.flush(),
})

Set up devtools

Install the devtools packages:

sh
npm install @tanstack/angular-devtools @tanstack/angular-pacer-devtools
npm install @tanstack/angular-devtools @tanstack/angular-pacer-devtools

Add the devtools provider in app.config.ts. You do not need to add a component to your template.

ts
import { isDevMode } from '@angular/core'
import { provideTanStackDevtools } from '@tanstack/angular-devtools/provider'
import { pacerDevtoolsPlugin } from '@tanstack/angular-pacer-devtools'
import type { ApplicationConfig } from '@angular/core'

export const appConfig: ApplicationConfig = {
  providers: [
    ...(isDevMode()
      ? [
          provideTanStackDevtools(() => ({
            plugins: [pacerDevtoolsPlugin()],
          })),
        ]
      : []),
  ],
}
import { isDevMode } from '@angular/core'
import { provideTanStackDevtools } from '@tanstack/angular-devtools/provider'
import { pacerDevtoolsPlugin } from '@tanstack/angular-pacer-devtools'
import type { ApplicationConfig } from '@angular/core'

export const appConfig: ApplicationConfig = {
  providers: [
    ...(isDevMode()
      ? [
          provideTanStackDevtools(() => ({
            plugins: [pacerDevtoolsPlugin()],
          })),
        ]
      : []),
  ],
}

A utility appears in the Pacer panel only when you give it a key option. See Devtools for production builds.

Next steps