TanStack
Getting Started

Vanilla Quick Start

TanStack Pacer controls when your functions run. Without a framework, use the core @tanstack/pacer package directly. It provides each utility both as a function that wraps your callback and as a class with methods and observable state.

This page starts with a debounced search input, then covers the patterns most apps need next. If you use a framework, start with its quick start instead. The adapters add lifecycle cleanup and reactive state on top of everything shown here.

Installation

shell
npm install @tanstack/pacer

See Installation for other package managers.

Your first debouncer

debounce wraps a function. The wrapped function waits until calls stop for wait milliseconds, then runs once with the latest arguments.

ts
import { debounce } from '@tanstack/pacer'

const input = document.querySelector<HTMLInputElement>('#search')!

const search = debounce(
  (query: string) => {
    console.log('Searching for', query)
  },
  { wait: 500 },
)

input.addEventListener('input', () => search(input.value))

Type "pacer" quickly and the console logs Searching for pacer once, 500 ms after the last keystroke.

Pick a function or a class

Every utility comes as a function and as a class:

UtilityFunctionClassAsync classGuide
DebouncingdebounceDebouncerAsyncDebouncerDebouncing
ThrottlingthrottleThrottlerAsyncThrottlerThrottling
Rate limitingrateLimitRateLimiterAsyncRateLimiterRate Limiting
QueuingqueueQueuerAsyncQueuerQueuing
BatchingbatchBatcherAsyncBatcherBatching

Each async class also has a matching function, such as asyncDebounce. Use the function when you only need to call the wrapped callback. Use the class when you need to flush, cancel, change options, or read state.

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

Common patterns

Control the debouncer directly

Debouncer exposes the same scheduling as debounce, plus methods to act on pending work:

ts
import { Debouncer } from '@tanstack/pacer'

const saver = new Debouncer((text: string) => saveDraft(text), { wait: 1000 })

editor.addEventListener('input', () => saver.maybeExecute(editor.value))
saveButton.addEventListener('click', () => saver.flush())
discardButton.addEventListener('click', () => saver.cancel())

flush runs pending work now. cancel drops it.

Read and subscribe to state

Every class keeps its state in a TanStack Store. Read the current state from store.state, and subscribe to changes with store.subscribe:

ts
const subscription = saver.store.subscribe(() => {
  status.textContent = saver.store.state.isPending ? 'Unsaved changes...' : ''
})

// Later, when the element goes away
subscription.unsubscribe()
saver.cancel()

Without a framework adapter, cleanup is your job. Unsubscribe and cancel pending work when the owning UI goes away. Each utility's guide lists the state fields it exposes.

Change options at runtime

Call setOptions with the options to change. It merges them into the current options:

ts
saver.setOptions({ wait: 2000 })

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.

Run async work

The async classes await your function, track execution state, and report errors:

ts
import { AsyncDebouncer } from '@tanstack/pacer'

const searcher = new AsyncDebouncer(
  async (term: string): Promise<Array<SearchResult>> => {
    const response = await fetch(`/api/search?q=${encodeURIComponent(term)}`, {
      signal: searcher.getAbortSignal() ?? undefined,
    })
    return response.json()
  },
  {
    wait: 300,
    onSuccess: (results) => renderResults(results),
    onError: (error) => console.error('Search failed:', error),
  },
)

input.addEventListener('input', () => searcher.maybeExecute(input.value))

maybeExecute returns a promise that resolves with your function's result. getAbortSignal() returns the signal for the current run, so searcher.abort() can stop the request. The async utilities also support retries. 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 { rateLimit } from '@tanstack/pacer'

const send = rateLimit((message: string) => sendMessage(message), {
  limit: 5,
  window: 60_000,
  onReject: (limiter) =>
    console.warn(`Try again in ${limiter.getMsUntilNextWindow()} ms`),
})

sendButton.addEventListener('click', () => send('Hello'))

Process items in order

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

ts
import { AsyncQueuer } from '@tanstack/pacer'

const uploads = new AsyncQueuer(async (file: File) => uploadFile(file), {
  concurrency: 3,
})

fileInput.addEventListener('change', () => {
  for (const file of fileInput.files ?? []) uploads.addItem(file)
})

Share options

Option helpers such as debouncerOptions return the object you pass in, typed for that utility. Use them to define options once and reuse them:

ts
import { Debouncer, debouncerOptions } from '@tanstack/pacer'

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

const debouncer = new Debouncer(search, { ...searchOptions, key: 'search' })

Import only what you use

The package root tree-shakes. Each utility also has its own entry point, such as @tanstack/pacer/debouncer, for bundlers or libraries that need a guaranteed small import.

If you publish a library and need the smallest possible footprint, see Pacer Lite in the Overview. It drops reactive state and devtools support in exchange for a smaller bundle.

Set up devtools

Install the devtools packages:

shell
npm install @tanstack/devtools @tanstack/pacer-devtools

Mount TanStackDevtoolsCore with plugins: [pacerDevtoolsPlugin()] once in your application. The devtools setup shows a mount function you can reuse. A utility appears in the Pacer panel only when you give it a key option.

Next steps