← Back to list

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…

Shehzad Ahmed · 2026-01-07 15:18 · 1 claps · 4.1 min read
#tanstack #batching #debounce #programming #javascript
Open on Medium ↗
Wiki topics: TLS · Design Tools & Workflow 💻 · Programming 🌐 · Web Development

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

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