Debouncing delays a function until calls have stopped for a configured amount of time. Each new call restarts the timer. With the default settings, only the most recent call executes, using its arguments.
Use debouncing when intermediate calls can be discarded and the final value is what matters. Search inputs, form validation, autosave, and resize handling are common examples.
The timeline below shows calls arriving in bursts. Every call resets the timer. The final call in each burst executes after three ticks of inactivity.
Debouncing (wait: 3 ticks)
Timeline: [1 second per tick]
Calls: ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️
Executed: ❌ ❌ ❌ ❌ ❌ ❌ ❌ ❌ ⏳ -> ✅ ❌ ⏳ -> ✅
[================================================================]
^ Executes here after
3 ticks of no calls
[Burst of calls] [More calls] [Wait] [New burst]
No execution Resets timer Execute Reset and executeOnly the latest call in each burst executes. All earlier calls are discarded.
Debouncing is intentionally lossy. If every operation must run, use queuing instead.
Choose debouncing when:
Choose another utility when:
The adapter provides two levels of debouncing API:
Create utilities as fields or during construction of a Lit element, and pass the element as the first argument. The snippets below belong to that element. Reactive options refresh when its host updates.
The snippets use application functions such as saveDraft and updateSearchResults. Supply those functions in your component.
Use createDebouncer(...).maybeExecute when an event should invoke a debounced side effect:
import { createDebouncer } from '@tanstack/lit-pacer'
// Fields on a LitElement:
search = createDebouncer(
this,
(query: string) => updateSearchResults(query),
{
wait: 500,
},
).maybeExecute
onInput = (event: Event) => {
this.search((event.target as HTMLInputElement).value)
}Keep the utility instance when the component also needs cancel() or flush(). Its bound maybeExecute method can be passed directly as an event handler.
Use createDebouncedState when Pacer should own the delayed state, or createDebouncedValue when a value already changes elsewhere:
import { createDebouncedValue } from '@tanstack/lit-pacer'
// query is a reactive property on this LitElement.
delayed = createDebouncedValue(this, () => this.query, { wait: 500 })
// Read this.delayed[0]() in render().import { createDebouncer } from '@tanstack/lit-pacer'
debouncer = createDebouncer(this, saveDraft, { wait: 500 }, (state) => ({
isPending: state.isPending,
}))
// In render():
// html`<button ?disabled=${!this.debouncer.state.isPending}
// @click=${() => this.debouncer.flush()}>Save now</button>`The bound maybeExecute() method returns void. The synchronous adapter does not retain return values or catch errors. Handle errors inside a trailing callback, or use async debouncing when the caller needs a Promise result.
The following timing and control snippets use the debouncer instance created in your component. Run those operations from event handlers or other application code.
The leading and trailing options control which edge of the wait period may execute.
| leading | trailing | Behavior |
|---|---|---|
| false | true | Wait for inactivity, then execute the most recent call. This is the default. |
| true | false | Execute the first call immediately. Later calls do not execute and restart the wait period. |
| true | true | Execute the first call immediately. If another call arrives during the wait period, execute the most recent call at the trailing edge. |
| false | false | Do not execute any calls. |
debouncer.setOptions({
wait: 1000,
leading: true,
trailing: true,
})
debouncer.maybeExecute('first') // Executes immediately.
debouncer.maybeExecute('second')
debouncer.maybeExecute('latest') // Executes after 1 second of inactivity.With both edges enabled, a single call executes only on the leading edge. A trailing execution occurs only when another call arrives during the wait period.
createDebouncer does not provide a maxWait option. A continuous stream of calls can keep postponing a trailing execution indefinitely. Use throttling when work must continue at a bounded interval while calls are still arriving.
The instance API distinguishes between executing, canceling, and resetting pending work.
flush() immediately executes the pending trailing call with the most recent arguments. It does nothing when no trailing call is pending.
debouncer.setOptions({ wait: 1000 })
debouncer.maybeExecute('draft')
debouncer.flush() // Executes saveDraft('draft') now.cancel() clears the pending timeout without executing the function. It also allows a leading call to execute immediately the next time maybeExecute() is called.
debouncer.maybeExecute('discarded draft')
debouncer.cancel()reset() restores the debouncer's state counters and flags to their defaults. It does not clear an already scheduled timeout. Call cancel() first when you need to discard pending work and reset state.
debouncer.cancel()
debouncer.reset()Use setOptions() to change options after construction:
debouncer.setOptions({
wait: 1000,
leading: true,
trailing: false,
})A new wait value applies when the next call schedules a timeout. It does not reschedule a timeout that is already pending. Calling maybeExecute() again clears the old timeout and schedules a new one using the current options.
Set enabled to false to prevent execution. Disabling a debouncer through setOptions() also cancels its pending call.
debouncer.setOptions({
wait: 500,
enabled: false,
})
debouncer.maybeExecute('ignored')
debouncer.setOptions({ enabled: true })
debouncer.maybeExecute('saved')The enabled and wait options may also be functions that receive the debouncer instance:
debouncer.setOptions({
enabled: (debouncer) => debouncer.store.state.executionCount < 10,
wait: (debouncer) => (debouncer.store.state.executionCount === 0 ? 300 : 500),
})Use onExecute for a side effect after the wrapped function runs. The callback receives the executed arguments followed by the debouncer instance.
debouncer.setOptions({
wait: 500,
onExecute: (args, debouncer) => {
console.log('Saved arguments:', args)
console.log('Execution count:', debouncer.store.state.executionCount)
},
})Pass an options factory or property getters to read reactive settings. Local options override provider defaults, and option changes retain the same utility instance.
// wait is a reactive property on this LitElement.
debouncer = createDebouncer(this, saveDraft, () => ({ wait: this.wait }))Disconnecting the Lit host cancels pending work and unsubscribes from state. Reconnection refreshes options and restores subscriptions to the same utility. Providing onUnmount replaces the default cleanup, so a custom callback must perform every required lifecycle action. Flushing during teardown can run callbacks after the component has begun to be destroyed.
Pass a selector to subscribe to the state your component reads. Without a selector, selected state is an empty object. Read the selection through this.debouncer.state. The core store remains available even when you do not select state for rendering.
Option functions and lifecycle callbacks receive the public utility instance. Reading .store.state in those callbacks is supported. Component rendering should read the selected adapter state.
A child can select state without subscribing the utility owner. The child subscription cleans up when its own scope or component is destroyed:
// In a child LitElement, using a utility supplied by its parent:
const selected = debouncer.subscribe(this, (state) => ({
isPending: state.isPending,
}))
// Read selected().isPending in the child's render().To restore selected state that your app has persisted, pass a partial snapshot through initialState. It is merged with the defaults. Restore only durable fields; pending timers are not restored.
See the Lit API reference for adapter signatures and the adapter guide for provider and helper return shapes.