throttle
Throttler
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 Throttling concepts of TanStack Pacer.
Throttling ensures function executions are evenly spaced over time. Unlike rate limiting which allows bursts of executions up to a limit, or debouncing which waits for activity to stop, throttling creates a smoother execution pattern by enforcing consistent delays between calls. If you set a throttle of one execution per second, calls will be spaced out evenly regardless of how rapidly they are requested.
Throttling (one execution per 3 ticks)
Timeline: [1 second per tick]
Calls: ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️
Executed: ✅ ❌ ⏳ -> ✅ ❌ ❌ ❌ ✅ ✅
[=================================================================]
^ Only one execution allowed per 3 ticks,
regardless of how many calls are made
[First burst] [More calls] [Spaced calls]
Execute first Execute after Execute each time
then throttle wait period wait period passes
Throttling (one execution per 3 ticks)
Timeline: [1 second per tick]
Calls: ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️
Executed: ✅ ❌ ⏳ -> ✅ ❌ ❌ ❌ ✅ ✅
[=================================================================]
^ Only one execution allowed per 3 ticks,
regardless of how many calls are made
[First burst] [More calls] [Spaced calls]
Execute first Execute after Execute each time
then throttle wait period wait period passes
Throttling is particularly effective when you need consistent, predictable execution timing. This makes it ideal for handling frequent events or updates where you want smooth, controlled behavior.
Common use cases include:
Throttling might not be the best choice when:
Tip
Throttling is often the best choice when you need smooth, consistent execution timing. It provides a more predictable execution pattern than rate limiting and more immediate feedback than debouncing.
TanStack Pacer provides both synchronous and asynchronous throttling through the Throttler and AsyncThrottler classes respectively (and their corresponding throttle and asyncThrottle functions).
The throttle function is the simplest way to add throttling to any function:
import { throttle } from '@tanstack/pacer'
// Throttle UI updates to once every 200ms
const throttledUpdate = throttle(
(value: number) => updateProgressBar(value),
{
wait: 200,
}
)
// In a rapid loop, only executes every 200ms
for (let i = 0; i < 100; i++) {
throttledUpdate(i) // Many calls get throttled
}
import { throttle } from '@tanstack/pacer'
// Throttle UI updates to once every 200ms
const throttledUpdate = throttle(
(value: number) => updateProgressBar(value),
{
wait: 200,
}
)
// In a rapid loop, only executes every 200ms
for (let i = 0; i < 100; i++) {
throttledUpdate(i) // Many calls get throttled
}
For more control over the throttling behavior, you can use the Throttler class directly:
import { Throttler } from '@tanstack/pacer'
const updateThrottler = new Throttler(
(value: number) => updateProgressBar(value),
{ wait: 200 }
)
// Get information about execution state
console.log(updateThrottler.getExecutionCount()) // Number of successful executions
console.log(updateThrottler.getLastExecutionTime()) // Timestamp of last execution
// Cancel any pending execution
updateThrottler.cancel()
import { Throttler } from '@tanstack/pacer'
const updateThrottler = new Throttler(
(value: number) => updateProgressBar(value),
{ wait: 200 }
)
// Get information about execution state
console.log(updateThrottler.getExecutionCount()) // Number of successful executions
console.log(updateThrottler.getLastExecutionTime()) // Timestamp of last execution
// Cancel any pending execution
updateThrottler.cancel()
The synchronous throttler supports both leading and trailing edge executions:
const throttledFn = throttle(fn, {
wait: 200,
leading: true, // Execute on first call (default)
trailing: true, // Execute after wait period (default)
})
const throttledFn = throttle(fn, {
wait: 200,
leading: true, // Execute on first call (default)
trailing: true, // Execute after wait period (default)
})
Common patterns:
The Throttler class supports enabling/disabling via the enabled option. Using the setOptions method, you can enable/disable the throttler at any time:
const throttler = new Throttler(fn, { wait: 200, enabled: false }) // Disable by default
throttler.setOptions({ enabled: true }) // Enable at any time
const throttler = new Throttler(fn, { wait: 200, enabled: false }) // Disable by default
throttler.setOptions({ enabled: true }) // Enable at any time
The enabled option can also be a function that returns a boolean, allowing for dynamic enabling/disabling based on runtime conditions:
const throttler = new Throttler(fn, {
wait: 200,
enabled: (throttler) => {
return throttler.getExecutionCount() < 50 // Disable after 50 executions
}
})
const throttler = new Throttler(fn, {
wait: 200,
enabled: (throttler) => {
return throttler.getExecutionCount() < 50 // Disable after 50 executions
}
})
If you are using a framework adapter where the throttler options are reactive, you can set the enabled option to a conditional value to enable/disable the throttler on the fly. However, if you are using the throttle function or the Throttler 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 Throttler class.
Several options in the Throttler support dynamic values through callback functions that receive the throttler instance:
const throttler = new Throttler(fn, {
// Dynamic wait time based on execution count
wait: (throttler) => {
return throttler.getExecutionCount() * 100 // Increase wait time with each execution
},
// Dynamic enabled state based on execution count
enabled: (throttler) => {
return throttler.getExecutionCount() < 50 // Disable after 50 executions
}
})
const throttler = new Throttler(fn, {
// Dynamic wait time based on execution count
wait: (throttler) => {
return throttler.getExecutionCount() * 100 // Increase wait time with each execution
},
// Dynamic enabled state based on execution count
enabled: (throttler) => {
return throttler.getExecutionCount() < 50 // Disable after 50 executions
}
})
The following options support dynamic values:
This allows for sophisticated throttling behavior that adapts to runtime conditions.
Both the synchronous and asynchronous throttlers support callback options to handle different aspects of the throttling lifecycle:
The synchronous Throttler supports the following callback:
const throttler = new Throttler(fn, {
wait: 200,
onExecute: (throttler) => {
// Called after each successful execution
console.log('Function executed', throttler.getExecutionCount())
}
})
const throttler = new Throttler(fn, {
wait: 200,
onExecute: (throttler) => {
// Called after each successful execution
console.log('Function executed', throttler.getExecutionCount())
}
})
The onExecute callback is called after each successful execution of the throttled function, making it useful for tracking executions, updating UI state, or performing cleanup operations.
The asynchronous AsyncThrottler supports additional callbacks for error handling:
const asyncThrottler = new AsyncThrottler(async (value) => {
await saveToAPI(value)
}, {
wait: 200,
onExecute: (throttler) => {
// Called after each successful execution
console.log('Async function executed', throttler.getExecutionCount())
},
onError: (error) => {
// Called if the async function throws an error
console.error('Async function failed:', error)
}
})
const asyncThrottler = new AsyncThrottler(async (value) => {
await saveToAPI(value)
}, {
wait: 200,
onExecute: (throttler) => {
// Called after each successful execution
console.log('Async function executed', throttler.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 throttler, while the onError callback allows you to handle errors gracefully without breaking the throttling chain. These callbacks are particularly useful for tracking execution counts, updating UI state, handling errors, performing cleanup operations, and logging execution metrics.
The async throttler provides a powerful way to handle asynchronous operations with throttling, offering several key advantages over the synchronous version. While the synchronous throttler is great for UI events and immediate feedback, the async version is specifically designed for handling API calls, database operations, and other asynchronous tasks.
Return Value Handling Unlike the synchronous throttler which returns void, the async version allows you to capture and use the return value from your throttled function. This is particularly useful when you need to work with the results of API calls or other async operations. The maybeExecute method returns a Promise that resolves with the function's return value, allowing you to await the result and handle it appropriately.
Error Handling The async throttler provides robust error handling capabilities:
Both the Async and Synchronous throttlers support the onExecute callback for handling successful executions.
For example, if you're updating a user's profile and then immediately fetching their updated data, you can await the update operation before starting the fetch:
Here's a basic example showing how to use the async throttler for a search operation:
const throttledSearch = asyncThrottle(
async (searchTerm: string) => {
const results = await fetchSearchResults(searchTerm)
return results
},
{
wait: 500,
onSuccess: (results, throttler) => {
console.log('Search succeeded:', results)
},
onError: (error, throttler) => {
console.error('Search failed:', error)
}
}
)
// Usage
try {
const results = await throttledSearch('query')
// Handle successful results
} catch (error) {
// Handle errors if no onError handler was provided
console.error('Search failed:', error)
}
const throttledSearch = asyncThrottle(
async (searchTerm: string) => {
const results = await fetchSearchResults(searchTerm)
return results
},
{
wait: 500,
onSuccess: (results, throttler) => {
console.log('Search succeeded:', results)
},
onError: (error, throttler) => {
console.error('Search failed:', error)
}
}
)
// Usage
try {
const results = await throttledSearch('query')
// Handle successful results
} catch (error) {
// Handle errors if no onError handler was provided
console.error('Search failed:', error)
}
Each framework adapter provides hooks that build on top of the core throttling functionality to integrate with the framework's state management system. Hooks like createThrottler, useThrottledCallback, useThrottledState, or useThrottledValue are available for each framework.
Here are some examples:
import { useThrottler, useThrottledCallback, useThrottledValue } from '@tanstack/react-pacer'
// Low-level hook for full control
const throttler = useThrottler(
(value: number) => updateProgressBar(value),
{ wait: 200 }
)
// Simple callback hook for basic use cases
const handleUpdate = useThrottledCallback(
(value: number) => updateProgressBar(value),
{ wait: 200 }
)
// State-based hook for reactive state management
const [instantState, setInstantState] = useState(0)
const [throttledValue] = useThrottledValue(
instantState, // Value to throttle
{ wait: 200 }
)
import { useThrottler, useThrottledCallback, useThrottledValue } from '@tanstack/react-pacer'
// Low-level hook for full control
const throttler = useThrottler(
(value: number) => updateProgressBar(value),
{ wait: 200 }
)
// Simple callback hook for basic use cases
const handleUpdate = useThrottledCallback(
(value: number) => updateProgressBar(value),
{ wait: 200 }
)
// State-based hook for reactive state management
const [instantState, setInstantState] = useState(0)
const [throttledValue] = useThrottledValue(
instantState, // Value to throttle
{ wait: 200 }
)
import { createThrottler, createThrottledSignal } from '@tanstack/solid-pacer'
// Low-level hook for full control
const throttler = createThrottler(
(value: number) => updateProgressBar(value),
{ wait: 200 }
)
// Signal-based hook for state management
const [value, setValue, throttler] = createThrottledSignal(0, {
wait: 200,
onExecute: (throttler) => {
console.log('Total executions:', throttler.getExecutionCount())
}
})
import { createThrottler, createThrottledSignal } from '@tanstack/solid-pacer'
// Low-level hook for full control
const throttler = createThrottler(
(value: number) => updateProgressBar(value),
{ wait: 200 }
)
// Signal-based hook for state management
const [value, setValue, throttler] = createThrottledSignal(0, {
wait: 200,
onExecute: (throttler) => {
console.log('Total executions:', throttler.getExecutionCount())
}
})
Each framework adapter provides hooks that integrate with the framework's state management system while maintaining the core throttling functionality.
Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.