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.
npm install @tanstack/pacernpm install @tanstack/pacerSee Installation for other package managers.
debounce wraps a function. The wrapped function waits until calls stop for wait milliseconds, then runs once with the latest arguments.
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))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.
Every utility comes as a function and as a class:
| Utility | Function | Class | Async class | Guide |
|---|---|---|---|---|
| Debouncing | debounce | Debouncer | AsyncDebouncer | Debouncing |
| Throttling | throttle | Throttler | AsyncThrottler | Throttling |
| Rate limiting | rateLimit | RateLimiter | AsyncRateLimiter | Rate Limiting |
| Queuing | queue | Queuer | AsyncQueuer | Queuing |
| Batching | batch | Batcher | AsyncBatcher | Batching |
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?.
Debouncer exposes the same scheduling as debounce, plus methods to act on pending work:
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())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.
Every class keeps its state in a TanStack Store. Read the current state from store.state, and subscribe to changes with store.subscribe:
const subscription = saver.store.subscribe(() => {
status.textContent = saver.store.state.isPending ? 'Unsaved changes...' : ''
})
// Later, when the element goes away
subscription.unsubscribe()
saver.cancel()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.
Call setOptions with the options to change. It merges them into the current options:
saver.setOptions({ wait: 2000 })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.
The async classes await your function, track execution state, and report errors:
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))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.
A rate limiter allows a fixed number of calls per window and rejects the rest:
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'))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'))A queuer keeps every item and processes them in order. With AsyncQueuer, concurrency sets how many run at once:
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)
})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)
})Option helpers such as debouncerOptions return the object you pass in, typed for that utility. Use them to define options once and reuse them:
import { Debouncer, debouncerOptions } from '@tanstack/pacer'
const searchOptions = debouncerOptions({ wait: 500, leading: false })
const debouncer = new Debouncer(search, { ...searchOptions, key: 'search' })import { Debouncer, debouncerOptions } from '@tanstack/pacer'
const searchOptions = debouncerOptions({ wait: 500, leading: false })
const debouncer = new Debouncer(search, { ...searchOptions, key: 'search' })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.
Install the devtools packages:
npm install @tanstack/devtools @tanstack/pacer-devtoolsnpm install @tanstack/devtools @tanstack/pacer-devtoolsMount 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.