agentsclimarketplace

Pacer patterns

Skill smicolon/ai-kit/packs/tanstack-router/skills/pacer-patterns

TanStack Pacer patterns for rate limiting, debouncing, and throttling. Activates when implementing search inputs, API rate limiting, or performance optimization. NOTE: Beta library - API may change.From its SKILL.md

Install
npx -y skills add smicolon/ai-kit --skill pacer-patterns

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 6 stars6 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

7.9 KB, ~2.0k tokens by cl100k_base, as published. Nobody here has run it

TanStack Pacer Patterns (Beta)

Beta Library: TanStack Pacer is in beta. APIs may change between versions.

TanStack Pacer provides utilities for rate limiting, debouncing, throttling, and async queuing.

Debounce

Delay execution until input stops for a specified time.

Basic Debounce

import { debounce } from '@tanstack/pacer'

const debouncedSearch = debounce((query: string) => {
  console.log('Searching:', query)
  return fetch(`/api/search?q=${query}`)
}, 300)

// Only executes after 300ms of no calls
debouncedSearch('h')
debouncedSearch('he')
debouncedSearch('hel')
debouncedSearch('hell')
debouncedSearch('hello') // Only this executes

Debounce in React

import { useDebounce } from '@tanstack/pacer-react'
import { useState } from 'react'

function SearchInput() {
  const [query, setQuery] = useState('')

  const debouncedQuery = useDebounce(query, 300)

  // Use debouncedQuery for API calls
  const { data } = useQuery({
    queryKey: ['search', debouncedQuery],
    queryFn: () => searchApi.search(debouncedQuery),
    enabled: debouncedQuery.length > 0,
  })

  return (
    <div>
      <input
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search..."
      />
      {data && <SearchResults results={data} />}
    </div>
  )
}

Debounced Callback

import { useDebouncedCallback } from '@tanstack/pacer-react'

function AutoSaveEditor({ postId }: { postId: string }) {
  const [content, setContent] = useState('')
  const updatePost = useUpdatePost()

  const saveContent = useDebouncedCallback(
    async (content: string) => {
      await updatePost.mutateAsync({ id: postId, content })
    },
    1000, // Save after 1s of no typing
    { maxWait: 5000 } // But at least every 5s
  )

  const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
    const newContent = e.target.value
    setContent(newContent)
    saveContent(newContent)
  }

  return (
    <textarea value={content} onChange={handleChange} />
  )
}

Throttle

Limit execution to at most once per interval.

Basic Throttle

import { throttle } from '@tanstack/pacer'

const throttledScroll = throttle((position: number) => {
  console.log('Scroll position:', position)
}, 100)

// Executes at most once every 100ms
window.addEventListener('scroll', () => {
  throttledScroll(window.scrollY)
})

Throttle in React

import { useThrottledCallback } from '@tanstack/pacer-react'
import { useEffect } from 'react'

function ScrollTracker() {
  const trackScroll = useThrottledCallback(
    (position: number) => {
      analytics.track('scroll', { position })
    },
    500
  )

  useEffect(() => {
    const handleScroll = () => trackScroll(window.scrollY)
    window.addEventListener('scroll', handleScroll)
    return () => window.removeEventListener('scroll', handleScroll)
  }, [trackScroll])

  return null
}

Throttled API Calls

import { useThrottledCallback } from '@tanstack/pacer-react'

function LiveSearch() {
  const [results, setResults] = useState<Result[]>([])

  const search = useThrottledCallback(
    async (query: string) => {
      const data = await searchApi.search(query)
      setResults(data)
    },
    200, // At most 5 requests per second
    { leading: true, trailing: true }
  )

  return (
    <input
      onChange={(e) => search(e.target.value)}
      placeholder="Search..."
    />
  )
}

Rate Limiting

Control the rate of operations.

Rate Limiter

import { createRateLimiter } from '@tanstack/pacer'

const apiLimiter = createRateLimiter({
  limit: 10,      // 10 requests
  interval: 1000, // per second
})

async function fetchData(id: string) {
  await apiLimiter.acquire() // Wait if rate limited
  return fetch(`/api/data/${id}`)
}

// Safe to call rapidly - will be rate limited
await Promise.all(
  ids.map(id => fetchData(id))
)

Per-Key Rate Limiting

import { createKeyedRateLimiter } from '@tanstack/pacer'

const userLimiter = createKeyedRateLimiter({
  limit: 5,
  interval: 60000, // 5 requests per minute per user
})

async function handleUserAction(userId: string, action: Action) {
  const limiter = userLimiter.get(userId)
  if (!limiter.tryAcquire()) {
    throw new Error('Rate limited. Please try again later.')
  }
  return processAction(action)
}

Async Queue

Process async operations sequentially or with concurrency limits.

Sequential Queue

import { createAsyncQueue } from '@tanstack/pacer'

const uploadQueue = createAsyncQueue({
  concurrency: 1, // One at a time
})

async function uploadFiles(files: File[]) {
  const results = await Promise.all(
    files.map(file =>
      uploadQueue.add(() => uploadFile(file))
    )
  )
  return results
}

Concurrent Queue with Limit

import { createAsyncQueue } from '@tanstack/pacer'

const processQueue = createAsyncQueue({
  concurrency: 3, // Max 3 concurrent
})

function ProcessingStatus() {
  const { pending, active, completed } = processQueue.status

  return (
    <div>
      <span>Pending: {pending}</span>
      <span>Active: {active}</span>
      <span>Completed: {completed}</span>
    </div>
  )
}

Queue with Priority

import { createAsyncQueue } from '@tanstack/pacer'

const taskQueue = createAsyncQueue({
  concurrency: 2,
})

// Higher priority tasks run first
taskQueue.add(() => processTask('low'), { priority: 1 })
taskQueue.add(() => processTask('high'), { priority: 10 })
taskQueue.add(() => processTask('urgent'), { priority: 100 })

Integration with TanStack Query

import { useDebounce } from '@tanstack/pacer-react'
import { useQuery } from '@tanstack/react-query'

function SearchWithDebounce() {
  const [input, setInput] = useState('')
  const debouncedInput = useDebounce(input, 300)

  const { data, isLoading } = useQuery({
    queryKey: ['search', debouncedInput],
    queryFn: () => searchApi.search(debouncedInput),
    enabled: debouncedInput.length >= 2,
  })

  return (
    <div>
      <input
        value={input}
        onChange={(e) => setInput(e.target.value)}
        placeholder="Search (min 2 chars)..."
      />
      {isLoading && <Spinner />}
      {data && <Results items={data} />}
    </div>
  )
}

Common Use Cases

ScenarioSolution
Search inputDebounce 300ms
Auto-saveDebounce 1s with maxWait
Scroll trackingThrottle 100-200ms
API rate limitsRate limiter
File uploadsAsync queue with concurrency
Resize handlerThrottle 100ms

Conventions

  1. Debounce for inputs - Always debounce search/filter inputs
  2. Throttle for events - Use for scroll, resize, mousemove
  3. Rate limit APIs - Protect against abuse
  4. Queue heavy operations - Use for file uploads, bulk operations
  5. Cleanup - Cancel pending operations on unmount

Anti-Patterns

// ❌ WRONG: No debounce on search
function Search() {
  const [query, setQuery] = useState('')
  const { data } = useQuery({
    queryKey: ['search', query], // Fires on every keystroke!
    queryFn: () => search(query),
  })
}

// ✅ CORRECT: Debounced search
function Search() {
  const [query, setQuery] = useState('')
  const debouncedQuery = useDebounce(query, 300)
  const { data } = useQuery({
    queryKey: ['search', debouncedQuery],
    queryFn: () => search(debouncedQuery),
    enabled: debouncedQuery.length > 0,
  })
}

// ❌ WRONG: Creating debounce inside render
function Component() {
  const search = debounce(() => {}, 300) // New instance every render!
}

// ✅ CORRECT: Use hook
function Component() {
  const search = useDebouncedCallback(() => {}, 300)
}

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.