TanStack Pacer: Solving Debounce, Throttle, and Batching the Right Way
The TanStack team has been on an absolute roll lately. New libraries, new abstractions, and a clear pattern: take common problems we all…
TanStack Pacer: Solving Debounce, Throttle, and Batching the Right Way
The TanStack team has been on an absolute roll lately. New libraries, new abstractions, and a clear pattern: take common problems we all keep re-solving and turn them into first-class tools.

One of the most underrated additions in this wave is TanStack Pacer.
At first glance, it sounds simple. Debouncing. Throttling. Rate limiting. Stuff we’ve all written a dozen times before. But once you start using Pacer, you realize it’s not just about delaying function calls — it’s about making time-based behavior predictable, composable, and React-friendly.
In this article, we’ll walk through real-world problems Pacer solves, why naïve solutions break down, and how TanStack Pacer makes these patterns feel native.
The Classic Problem: Search Inputs That Spam Your API
Let’s start with a very common scenario: a search input.
Every time the user types a character, we fire off a request.
<input onChange={handleSearch} />
Seems harmless… until someone types five characters and you’ve made five network requests. Multiply that by hundreds of users and suddenly your backend isn’t very happy.
What we actually want is debouncing:
“Wait until the user stops typing, then send a single request.”
Sure, you can write your own debounce logic. But now you’re dealing with timers, cleanup, stale closures, and edge cases.
This is where TanStack Pacer shines.
Debouncing With useDebouncedCallback
Install the React adapter:
npm install @tanstack/react-pacer
Now let’s debounce a search request.
Before (naïve implementation)
function Search() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const controllerRef = useRef<AbortController | null>(null);
async function handleSearch(query: string) {
controllerRef.current?.abort();
controllerRef.current = new AbortController();
setLoading(true);
const res = await fetch(`/api/users?q=${query}`, {
signal: controllerRef.current.signal,
});
const data = await res.json();
setUsers(data);
setLoading(false);
}
return (
<input
onChange={(e) => handleSearch(e.target.value)}
placeholder="Search users"
/>
);
}
Every keystroke = one request.
After (debounced with TanStack Pacer)
import { useDebouncedCallback } from "@tanstack/react-pacer";
function Search() {
const [users, setUsers] = useState([]);
const [loading, setLoading] = useState(false);
const controllerRef = useRef<AbortController | null>(null);
const debouncedSearch = useDebouncedCallback(
async (query: string) => {
controllerRef.current?.abort();
controllerRef.current = new AbortController();
setLoading(true);
const res = await fetch(`/api/users?q=${query}`, {
signal: controllerRef.current.signal,
});
const data = await res.json();
setUsers(data);
setLoading(false);
},
{ wait: 1000 }
);
return (
<input
onChange={(e) => debouncedSearch(e.target.value)}
placeholder="Search users"
/>
);
}
Now:
- The user types freely
- Requests fire only after 1 second of inactivity
- One request instead of many
Clean. Predictable. Zero timer juggling.
When Debounce Isn’t Enough: Enter Throttling
Debouncing waits until things stop happening.
But sometimes, things never stop — like window resize or scroll events.
If you update state on every resize event, you’re going to trigger dozens (or hundreds) of renders per second.
That’s where throttling comes in:
“Only allow this to run once every X milliseconds.”
Throttling State Updates with useThrottledState
Problem: Resize spam
function ResizeExample() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handler = () => setWidth(window.innerWidth);
window.addEventListener("resize", handler);
return () => window.removeEventListener("resize", handler);
}, []);
return <h1>{width}</h1>;
}
Works — but it updates constantly.
Solution: Throttled state
import { useThrottledState } from "@tanstack/react-pacer";
function ResizeExample() {
const [width, setWidth] = useThrottledState(window.innerWidth, {
wait: 100,
});
useEffect(() => {
const handler = () => setWidth(window.innerWidth);
window.addEventListener("resize", handler);
return () => window.removeEventListener("resize", handler);
}, [setWidth]);
return <h1>{width}</h1>;
}
Now the width updates at most 10 times per second, instead of hundreds.
Perfect for:
- Resize listeners
- Scroll tracking
- Mouse movement
- Performance-heavy calculations
Instant Value + Throttled Value at the Same Time
Sometimes you want both:
- One value that updates instantly
- One value that updates on a delay
That’s exactly what useThrottledValue gives you.
import { useThrottledValue } from "@tanstack/react-pacer";
function ResizeWithComparison() {
const [width, setWidth] = useState(window.innerWidth);
const [throttledWidth] = useThrottledValue(width, {
wait: 1000,
});
useEffect(() => {
const handler = () => setWidth(window.innerWidth);
window.addEventListener("resize", handler);
return () => window.removeEventListener("resize", handler);
}, []);
return (
<>
<h2>Instant: {width}</h2>
<h2>Throttled: {throttledWidth}</h2>
</>
);
}
This pattern is incredibly useful when:
- UI needs instant feedback
- Expensive logic should update less often
Accessing Throttle State (isPending)
Every Pacer hook exposes its internal controller (debouncer / throttler).
You can subscribe to its state with a third argument.
const [throttledWidth, throttler] = useThrottledValue(
width,
{ wait: 1000 },
(state) => ({
isPending: state.isPending,
})
);
Then render it:
<h3>{throttler.state.isPending ? "Pending" : "Done"}</h3>
This makes it easy to:
- Show loading indicators
- Disable buttons
- Reflect async timing visually
Batching: Autosave Without Killing Your Server
Now for one of the most powerful use cases: batching.
Think Google Docs or Figma. They don’t save on every keystroke. They batch changes and send them together.
Problem: Saving on every update
useEffect(() => {
saveToServer(count);
}, [count]);
Every change = one request.
Solution: useBatchedCallback
import { useBatchedCallback } from "@tanstack/react-pacer";
function BatchedSaveExample() {
const [count, setCount] = useState(0);
const [serverCount, setServerCount] = useState(0);
const batchedSave = useBatchedCallback(
async (values: number[]) => {
// simulate server save
await new Promise((r) => setTimeout(r, 1000));
setServerCount(values[values.length - 1]);
},
{
wait: 2000,
maxSize: 5,
}
);
useEffect(() => {
batchedSave(count);
}, [count, batchedSave]);
return (
<>
<h2>Local Count: {count}</h2>
<h2>Server Count: {serverCount}</h2>
<button onClick={() => setCount((c) => c + 1)}>Increment</button>
</>
);
}
What’s happening here:
- Saves only after 5 changes, OR
- Saves after 2 seconds of inactivity
- Reduces server calls by up to 5x or more
This pattern is gold for:
- Autosave editors
- Analytics batching
- Offline sync
- High-frequency state updates
Final Thoughts
TanStack Pacer isn’t flashy. It doesn’t reinvent React. But it quietly solves problems we all keep re-solving badly.
What makes it special:
- Unified API for debounce, throttle, batching
- Hooks for callbacks, state, and values
- Full access to internal timing state
- Zero boilerplate timers
If you’re building real production apps — and not just demos — you need these patterns.
And now, you finally have a clean way to use them.
Reference Links
- Tanstack Pacer— Offical Tanstack Pacer Docs.
Find me on your favorite platform
- Github — Follow me on GitHub for further useful code snippets and open source repos.
- Instagram — Follow me on instagram to connect.
- LinkedIn Profile — Connect with me on LinkedIn for further discussions and updates.
- Twitter (X) — Connect with me on Twitter (X) for useless tech tweets.
메타데이터
- post_id
- 94d699befc8a
- slug
- tanstack-pacer-solving-debounce-throttle-and-batching-the-right-way-94d699befc8a
- url
- https://medium.com/@shaxadd/tanstack-pacer-solving-debounce-throttle-and-batching-the-right-way-94d699befc8a
- canonical_url
- https://medium.com/@shaxadd/tanstack-pacer-solving-debounce-throttle-and-batching-the-right-way-94d699befc8a
- author_url
- https://medium.com/@shaxadd
- status
- ok
- fetched_at
- 2026-06-09 15:37:30