Batching collects items and passes them to one function as an array. A batch can run when it reaches a configured size, after no new items arrive for a configured wait, or when custom logic says it is ready.
Batching reduces the number of operations by processing several items together. Unlike queuing, it does not call the wrapped function once for each item.
Batching (process every 3 items or after 2 quiet ticks)
Timeline: [1 second per tick]
Calls: ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️
Batch: [ABC] [] [DE] [] [FGH] []
Executed: ✅ ✅ ✅
[======================================================]
^ Items are grouped and processed together
[Size reached] [Wait elapsed] [Size reached]Batching (process every 3 items or after 2 quiet ticks)
Timeline: [1 second per tick]
Calls: ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️ ⬇️
Batch: [ABC] [] [DE] [] [FGH] []
Executed: ✅ ✅ ✅
[======================================================]
^ Items are grouped and processed together
[Size reached] [Wait elapsed] [Size reached]Each execution receives a copy of the items currently collected. The batcher clears those items before calling the wrapped function.
Choose batching when:
Choose another utility when:
Use the callback API when adding items is all the component needs. Use the instance API for flush(), cancel(), collected items, selected state, and dynamic options.
import { useBatcher } from '@tanstack/react-pacer'
function AnalyticsButton() {
const batcher = useBatcher(
sendEvents,
{ maxSize: 20, wait: 1000 },
(state) => ({
size: state.size,
}),
)
return (
<button onClick={() => batcher.addItem({ type: 'click' })}>
Track ({batcher.state.size} pending)
</button>
)
}import { useBatcher } from '@tanstack/react-pacer'
function AnalyticsButton() {
const batcher = useBatcher(
sendEvents,
{ maxSize: 20, wait: 1000 },
(state) => ({
size: state.size,
}),
)
return (
<button onClick={() => batcher.addItem({ type: 'click' })}>
Track ({batcher.state.size} pending)
</button>
)
}The focused snippets later in this guide use useBatcher and assume they run inside a component or another hook.
maxSize executes the batch as soon as the number of collected items reaches the limit.
const batcher = useBatcher(processBatch, {
maxSize: 100,
})const batcher = useBatcher(processBatch, {
maxSize: 100,
})The default is Infinity, so a size trigger is disabled unless you provide one.
wait executes a batch after no new items arrive for the configured duration. Every added item restarts the timer.
const batcher = useBatcher(processBatch, {
wait: 1000,
})const batcher = useBatcher(processBatch, {
wait: 1000,
})The default is Infinity, so a time trigger is disabled unless you provide one. A continuous stream of items can keep restarting the timer. Combine wait with maxSize when a batch must eventually run under continuous traffic.
getShouldExecute runs after each item is added. Return true to execute the current batch immediately.
const batcher = useBatcher<number>(processBatch, {
getShouldExecute: (items) => items.includes(0),
})
batcher.addItem(4)
batcher.addItem(0) // Executes [4, 0].const batcher = useBatcher<number>(processBatch, {
getShouldExecute: (items) => items.includes(0),
})
batcher.addItem(4)
batcher.addItem(0) // Executes [4, 0].If several triggers are configured, the first one reached executes the batch.
flush() clears the pending timer and executes all currently collected items. It does nothing when the batch is empty.
batcher.addItem('event-1')
batcher.addItem('event-2')
batcher.flush()batcher.addItem('event-1')
batcher.addItem('event-2')
batcher.flush()cancel() clears the pending timer but keeps the collected items. A later item can schedule a new timer, or you can call flush().
batcher.cancel()
console.log(batcher.peekAllItems()) // Items are still present.batcher.cancel()
console.log(batcher.peekAllItems()) // Items are still present.clear() removes all collected items. It does not clear the timer itself, although that timer has no items to execute unless more items are added.
batcher.clear()batcher.clear()reset() restores batch state and counters to their defaults. It does not cancel an already scheduled timer. Call cancel() before reset() when pending work must be discarded.
batcher.cancel()
batcher.reset()batcher.cancel()
batcher.reset()Use setOptions() to update future trigger behavior:
batcher.setOptions({
maxSize: 20,
wait: 500,
})batcher.setOptions({
maxSize: 20,
wait: 500,
})Changing wait does not reschedule an existing timer. The next addItem() call replaces that timer using the current value.
The wait option may be a function that receives the batcher instance:
const batcher = useBatcher(processBatch, {
wait: (batcher) => (batcher.store.state.size > 10 ? 100 : 500),
})const batcher = useBatcher(processBatch, {
wait: (batcher) => (batcher.store.state.size > 10 ? 100 : 500),
})Use onItemsChange to observe collection changes and onExecute to observe completed batch calls:
const batcher = useBatcher(processBatch, {
maxSize: 10,
onItemsChange: (batcher) => {
console.log('Collected:', batcher.store.state.size)
},
onExecute: (items, batcher) => {
console.log('Processed:', items)
console.log('Batches:', batcher.store.state.executionCount)
},
})const batcher = useBatcher(processBatch, {
maxSize: 10,
onItemsChange: (batcher) => {
console.log('Collected:', batcher.store.state.size)
},
onExecute: (items, batcher) => {
console.log('Processed:', items)
console.log('Batches:', batcher.store.state.executionCount)
},
})Do not use started to pause a batcher. It is currently a no-op, so every addItem() call evaluates the configured triggers.
The adapter cancels the pending wait timer while retaining collected items when its owner is destroyed. Providing onUnmount replaces that default cleanup, so a custom callback must perform every required lifecycle action. When custom cleanup flushes work, remember that user callbacks can run while the component is being destroyed.
The adapter subscribes only to the state returned by the selector argument. Without a selector, the adapter state is empty. Create the utility at the top level of a component or another hook and select only fields used by the view:
const batcher = useBatcher(
processBatch,
{ maxSize: 20, wait: 1000 },
(state) => ({
size: state.size,
isPending: state.isPending,
}),
)
console.log(batcher.state.size, batcher.state.isPending)const batcher = useBatcher(
processBatch,
{ maxSize: 20, wait: 1000 },
(state) => ({
size: state.size,
isPending: state.isPending,
}),
)
console.log(batcher.state.size, batcher.state.isPending)Option functions and lifecycle callbacks receive the underlying public utility instance. The .store.state reads inside those callbacks in the examples above are supported. Rendering code should read the selected adapter state shown here.
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.
Commonly useful state includes:
See the React API reference for adapter signatures and the public core reference for complete option and state types.