debounce
Debouncer
ClassRate Limiting, Throttling, and Debouncing are three distinct approaches to controlling function execution frequency. Each technique blocks executions differently, making them "lossy" - meaning some function calls will not execute when they are requested to run too frequently. Understanding when to use each approach is crucial for building performant and reliable applications. This guide will cover the Debouncing concepts of TanStack Pacer.
Debouncing is a technique that delays the execution of a function until a specified period of inactivity has occurred. Unlike rate limiting which allows bursts of executions up to a limit, or throttling which ensures evenly spaced executions, debouncing collapses multiple rapid function calls into a single execution that only happens after the calls stop. This makes debouncing ideal for handling bursts of events where you only care about the final state after the activity settles.
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 [Delayed Execute] [Wait] [Delayed Execute]
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 [Delayed Execute] [Wait] [Delayed Execute]
Debouncing is particularly effective when you want to wait for a "pause" in activity before taking action. This makes it ideal for handling user input or other rapidly-firing events where you only care about the final state.
Common use cases include:
Debouncing might not be the best choice when:
TanStack Pacer provides both synchronous and asynchronous debouncing through the Debouncer and AsyncDebouncer classes respectively (and their corresponding debounce and asyncDebounce functions).
The debounce function is the simplest way to add debouncing to any function:
import { debounce } from '@tanstack/pacer'
// Debounce search input to wait for user to stop typing
const debouncedSearch = debounce(
(searchTerm: string) => performSearch(searchTerm),
{
wait: 500, // Wait 500ms after last keystroke
}
)
searchInput.addEventListener('input', (e) => {
debouncedSearch(e.target.value)
})
import { debounce } from '@tanstack/pacer'
// Debounce search input to wait for user to stop typing
const debouncedSearch = debounce(
(searchTerm: string) => performSearch(searchTerm),
{
wait: 500, // Wait 500ms after last keystroke
}
)
searchInput.addEventListener('input', (e) => {
debouncedSearch(e.target.value)
})
For more control over the debouncing behavior, you can use the Debouncer class directly:
import { Debouncer } from '@tanstack/pacer'
const searchDebouncer = new Debouncer(
(searchTerm: string) => performSearch(searchTerm),
{ wait: 500 }
)
// Get information about current state
console.log(searchDebouncer.getExecutionCount()) // Number of successful executions
console.log(searchDebouncer.getIsPending()) // Whether a call is pending
// Update options dynamically
searchDebouncer.setOptions({ wait: 1000 }) // Increase wait time
// Cancel pending execution
searchDebouncer.cancel()
import { Debouncer } from '@tanstack/pacer'
const searchDebouncer = new Debouncer(
(searchTerm: string) => performSearch(searchTerm),
{ wait: 500 }
)
// Get information about current state
console.log(searchDebouncer.getExecutionCount()) // Number of successful executions
console.log(searchDebouncer.getIsPending()) // Whether a call is pending
// Update options dynamically
searchDebouncer.setOptions({ wait: 1000 }) // Increase wait time
// Cancel pending execution
searchDebouncer.cancel()
The synchronous debouncer supports both leading and trailing edge executions:
const debouncedFn = debounce(fn, {
wait: 500,
leading: true, // Execute on first call
trailing: true, // Execute after wait period
})
const debouncedFn = debounce(fn, {
wait: 500,
leading: true, // Execute on first call
trailing: true, // Execute after wait period
})
Common patterns:
The TanStack Pacer Debouncer does NOT have a maxWait option like other debouncing libraries. If you need to let executions run over a more spread out period of time, consider using the throttling technique instead.
The Debouncer class supports enabling/disabling via the enabled option. Using the setOptions method, you can enable/disable the debouncer at any time:
const debouncer = new Debouncer(fn, { wait: 500, enabled: false }) // Disable by default
debouncer.setOptions({ enabled: true }) // Enable at any time
const debouncer = new Debouncer(fn, { wait: 500, enabled: false }) // Disable by default
debouncer.setOptions({ enabled: true }) // Enable at any time
If you are using a framework adapter where the debouncer options are reactive, you can set the enabled option to a conditional value to enable/disable the debouncer on the fly:
// React example
const debouncer = useDebouncer(
setSearch,
{ wait: 500, enabled: searchInput.value.length > 3 } // Enable/disable based on input length IF using a framework adapter that supports reactive options
)
// React example
const debouncer = useDebouncer(
setSearch,
{ wait: 500, enabled: searchInput.value.length > 3 } // Enable/disable based on input length IF using a framework adapter that supports reactive options
)
However, if you are using the debounce function or the Debouncer class directly, you must use the setOptions method to change the enabled option, since the options that are passed are actually passed to the constructor of the Debouncer class.
// Solid example
const debouncer = new Debouncer(fn, { wait: 500, enabled: false }) // Disable by default
createEffect(() => {
debouncer.setOptions({ enabled: search().length > 3 }) // Enable/disable based on input length
})
// Solid example
const debouncer = new Debouncer(fn, { wait: 500, enabled: false }) // Disable by default
createEffect(() => {
debouncer.setOptions({ enabled: search().length > 3 }) // Enable/disable based on input length
})
Both the synchronous and asynchronous debouncers support callback options to handle different aspects of the debouncing lifecycle:
The synchronous Debouncer supports the following callback:
const debouncer = new Debouncer(fn, {
wait: 500,
onExecute: (debouncer) => {
// Called after each successful execution
console.log('Function executed', debouncer.getExecutionCount())
}
})
const debouncer = new Debouncer(fn, {
wait: 500,
onExecute: (debouncer) => {
// Called after each successful execution
console.log('Function executed', debouncer.getExecutionCount())
}
})
The onExecute callback is called after each successful execution of the debounced function, making it useful for tracking executions, updating UI state, or performing cleanup operations.
The asynchronous AsyncDebouncer supports additional callbacks for error handling:
const asyncDebouncer = new AsyncDebouncer(async (value) => {
await saveToAPI(value)
}, {
wait: 500,
onExecute: (debouncer) => {
// Called after each successful execution
console.log('Async function executed', debouncer.getExecutionCount())
},
onError: (error) => {
// Called if the async function throws an error
console.error('Async function failed:', error)
}
})
const asyncDebouncer = new AsyncDebouncer(async (value) => {
await saveToAPI(value)
}, {
wait: 500,
onExecute: (debouncer) => {
// Called after each successful execution
console.log('Async function executed', debouncer.getExecutionCount())
},
onError: (error) => {
// Called if the async function throws an error
console.error('Async function failed:', error)
}
})
The onExecute callback works the same way as in the synchronous debouncer, while the onError callback allows you to handle errors gracefully without breaking the debouncing chain. These callbacks are particularly useful for tracking execution counts, updating UI state, handling errors, performing cleanup operations, and logging execution metrics.
For async functions or when you need error handling, use the AsyncDebouncer or asyncDebounce:
import { asyncDebounce } from '@tanstack/pacer'
const debouncedSearch = asyncDebounce(
async (searchTerm: string) => {
const results = await fetchSearchResults(searchTerm)
updateUI(results)
},
{
wait: 500,
onError: (error) => {
console.error('Search failed:', error)
}
}
)
// Will only make one API call after typing stops
searchInput.addEventListener('input', async (e) => {
await debouncedSearch(e.target.value)
})
import { asyncDebounce } from '@tanstack/pacer'
const debouncedSearch = asyncDebounce(
async (searchTerm: string) => {
const results = await fetchSearchResults(searchTerm)
updateUI(results)
},
{
wait: 500,
onError: (error) => {
console.error('Search failed:', error)
}
}
)
// Will only make one API call after typing stops
searchInput.addEventListener('input', async (e) => {
await debouncedSearch(e.target.value)
})
The async version provides Promise-based execution tracking, error handling through the onError callback, proper cleanup of pending async operations, and an awaitable maybeExecute method.
Each framework adapter provides hooks that build on top of the core debouncing functionality to integrate with the framework's state management system. Hooks like createDebouncer, useDebouncedCallback, useDebouncedState, or useDebouncedValue are available for each framework.
Here are some examples:
import { useDebouncer, useDebouncedCallback, useDebouncedValue } from '@tanstack/react-pacer'
// Low-level hook for full control
const debouncer = useDebouncer(
(value: string) => saveToDatabase(value),
{ wait: 500 }
)
// Simple callback hook for basic use cases
const handleSearch = useDebouncedCallback(
(query: string) => fetchSearchResults(query),
{ wait: 500 }
)
// State-based hook for reactive state management
const [instantState, setInstantState] = useState('')
const [debouncedState, setDebouncedState] = useDebouncedValue(
instantState, // Value to debounce
{ wait: 500 }
)
import { useDebouncer, useDebouncedCallback, useDebouncedValue } from '@tanstack/react-pacer'
// Low-level hook for full control
const debouncer = useDebouncer(
(value: string) => saveToDatabase(value),
{ wait: 500 }
)
// Simple callback hook for basic use cases
const handleSearch = useDebouncedCallback(
(query: string) => fetchSearchResults(query),
{ wait: 500 }
)
// State-based hook for reactive state management
const [instantState, setInstantState] = useState('')
const [debouncedState, setDebouncedState] = useDebouncedValue(
instantState, // Value to debounce
{ wait: 500 }
)
import { createDebouncer, createDebouncedSignal } from '@tanstack/solid-pacer'
// Low-level hook for full control
const debouncer = createDebouncer(
(value: string) => saveToDatabase(value),
{ wait: 500 }
)
// Signal-based hook for state management
const [searchTerm, setSearchTerm, debouncer] = createDebouncedSignal('', {
wait: 500,
onExecute: (debouncer) => {
console.log('Total executions:', debouncer.getExecutionCount())
}
})
import { createDebouncer, createDebouncedSignal } from '@tanstack/solid-pacer'
// Low-level hook for full control
const debouncer = createDebouncer(
(value: string) => saveToDatabase(value),
{ wait: 500 }
)
// Signal-based hook for state management
const [searchTerm, setSearchTerm, debouncer] = createDebouncedSignal('', {
wait: 500,
onExecute: (debouncer) => {
console.log('Total executions:', debouncer.getExecutionCount())
}
})
Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.