TanStack Pacer

repository·main·Indexed 20 days ago

https://github.com/tanstack/pacer

A lightweight timing and scheduling library for debouncing, throttling, rate limiting, queuing, and batching. Designed primarily for the client-side, it focuses on fine-grained reactivity and type safety.

Tokens
235.6K
Snippets
738
Records
1.1K
Agent score
69%

What's inside TanStack Pacer

  1. What is TanStack Pacer?

    main

    TanStack Pacer is a framework-agnostic library designed to control the timing and execution of function calls. It provides high-quality, type-safe, and tree-shakable utilities to prevent performance issues, race conditions, and poor user experiences caused by uncontrolled function execution.

    Key capabilities include:

    • Debouncing: Delay execution until after a period of inactivity.
    • Throttling: Limit the rate at which a function fires.
    • Rate Limiting: Limit execution over a specific period (Fixed or Sliding Window).
    • Queuing: Execute functions in FIFO, LIFO, or Priority order with concurrency and expiration controls.
    • Batching: Group multiple operations into chunks based on time, size, or custom conditions.

    Note: The library is currently in beta and the API is subject to change.

  2. Overview of TanStack Pacer

    main

    TanStack Pacer is a lightweight timing and scheduling library designed for managing the execution frequency and order of functions. It provides utilities for common scheduling patterns such as debouncing, throttling, rate limiting, queuing, and batching.

    Key features include:

    • Scheduling Utilities: Support for both synchronous and asynchronous versions of debounce, throttle, rate limiting, queuing, and batching.
    • Advanced Control: Options for leading/trailing execution, concurrency limits, priority queues (FIFO, LIFO), and expiration durations.
    • State Management: Built on top of TanStack Store for fine-grained reactivity, allowing for easy integration with existing state management libraries and persistence to local/session storage.
    • Type Safety: Full TypeScript support with generics to ensure functions are called with correct arguments.
    • Framework Support: Includes adapters for React, Solid, Preact, and Angular.
    • Tree Shaking: Optimized for bundle size with support for deep imports to minimize impact when embedding in other libraries.
  3. Compare TanStack Pacer utilities

    main

    TanStack Pacer provides five primary utilities for controlling execution frequency and timing. Choosing the right one depends on your specific use case:

    • useDebouncer: Delays execution until a specified period of inactivity has passed.
      • Best for: Search inputs (waits for the user to stop typing).
    • useThrottler: Limits execution to once per specified time period, typically executing immediately and then blocking subsequent calls.
      • Best for: Scroll or resize events (provides immediate response with controlled frequency).
    • useRateLimiter: Limits the maximum number of executions allowed within a specific time window.
      • Best for: API calls (prevents overwhelming external services).
    • useQueuer: Processes items sequentially, often with optional delays between them.
      • Best for: Sequential operations (maintains order and prevents overlapping tasks).
    • useBatcher: Collects multiple items and processes them together in groups (batches) based on a size limit or a time limit.
      • Best for: Bulk operations (efficiently processing multiple items at once).
  4. What is debouncing and when to use it

    main

    Debouncing is a technique that delays function execution until a specified period of inactivity has occurred. It collapses multiple rapid function calls into a single execution that only happens after the calls stop.

    When to use it:

    • Handling user input (e.g., search bars) where you only care about the final state after activity settles.
    • Handling rapidly-firing events where you want to wait for a "pause" before taking action.

    When NOT to use it:

    • If you need guaranteed execution over a specific time period (use throttling instead).
    • If you cannot afford to miss any executions (use queuing instead).
  5. What is Batching and when to use it

    main

    Batching is a technique that collects multiple operations over time or until a specific threshold is met, then processes them as a single unit. This is more efficient than queuing when processing items in bulk reduces overhead (e.g., network requests, database writes).

    Use Batching when:

    • Processing items in groups is more efficient.
    • You want to reduce the frequency of expensive operations.
    • You need to control the rate or size of processing.
    • You want to debounce bursts of activity.

    Do NOT use Batching when:

    • Every item must be processed individually and immediately (use queuing).
    • You only care about the most recent value (use debouncing).
  6. What is Async Queuing and when to use it

    main

    Async queuing implements a 'task pool' or 'worker pool' pattern, allowing multiple asynchronous operations (like I/O or network requests) to be processed concurrently. This differs from standard queuing by enabling multiple items to be in an 'Active' state simultaneously while maintaining control over concurrency and timing.

    Use Async Queuing when you need to:

    • Process multiple asynchronous operations concurrently.
    • Control the number of simultaneous operations.
    • Handle Promise-based tasks with robust error handling.
    • Maintain order while maximizing throughput.
    • Process background tasks that can run in parallel.

    Do NOT use Async Queuing if:

    • You don't need concurrent processing (use Queuing instead).
    • You don't need all queued executions to run (use Throttling instead).
    • You want to group operations together (use Batching instead).
  7. Optimize useDebouncedState re-renders with a selector

    main

    To optimize performance and prevent unnecessary re-renders, useDebouncedState requires an explicit selector to subscribe to debouncer state changes. Without a selector, the component only re-renders when the debounced value itself changes.

    Use the selector to pick exactly which properties of the debouncer state should trigger a component update.

    Common Selector Patterns

    Track loading/pending state:

    (state) => ({ isPending: state.isPending })

    Track execution count:

    (state) => ({ executionCount: state.executionCount })

    Track debouncing status and leading edge capability:

    (state) => ({
      status: state.status,
      canLeadingExecute: state.canLeadingExecute
    })
    // Example: Re-render only when status or leading edge capability changes
    const [searchTerm, setSearchTerm, debouncer] = useDebouncedState(
      '',
      { wait: 500 },
      (state) => ({
        status: state.status,
        canLeadingExecute: state.canLeadingExecute
      })
    );
  8. Access reactive state in Preact

    main

    When using PreactAsyncQueuer, you have two ways to access the state, but they behave differently regarding reactivity:

    1. queuer.state (Recommended): This provides reactive state that automatically triggers Preact re-renders when the queuer state changes.
    2. queuer.store.state (Deprecated for reactivity): The state on the store object is not reactive by default because it has not been wrapped in a useSelector hook internally.

    If you must use the store object but require reactivity, you must manually wrap your usage with a useSelector hook.

  9. Configure error handling in AsyncQueuer

    main

    You can control how errors are handled during task execution using onError and throwOnError options:

    • onError: A callback function called with the error and the queuer instance. If provided, the error is swallowed by default (unless throwOnError is also true).
    • throwOnError: A boolean (defaults to true if no onError handler is provided). If true, the error will be thrown.

    Behavior Matrix:

    onError provided?throwOnErrorResult
    YesTrueHandler called, then error is thrown
    YesFalseHandler called, error is swallowed
    NoTrueError is thrown
    NoFalse(Not applicable, throwOnError defaults to true if no handler exists)

    You can also check the error state directly on the AsyncQueuer instance.

  10. Configure state tracking with selector in injectAsyncRateLimiter

    main
    By default, injectAsyncRateLimiter does not subscribe to reactive state changes. To make the rate limiter's state reactive within your Angular component (e.g., to drive UI updates via signals), you must provide a selector function. The selector allows you to specify which specific parts of the state should trigger signal updates, preventing unnecessary re-renders when irrelevant state changes occur.
  11. Manage state subscriptions with useAsyncRateLimiter

    main

    By default, useAsyncRateLimiter does not trigger re-renders. You must opt-in to reactive state tracking to prevent unnecessary performance overhead. You can subscribe to state in two ways:

    1. Using the selector parameter: Pass a selector function as the third argument to the hook. The component using the hook will only re-render when the selected slice of state changes. This is best for local component state (e.g., loading indicators or data display).
    2. Using the Subscribe HOC: Use asyncRateLimiter.Subscribe to subscribe to state changes deep in your component tree. This avoids "prop drilling" the rate limiter instance and is ideal for child components.

    Available State Properties

    • errorCount: Number of executions resulting in errors.
    • executionTimes: Array of timestamps of executions.
    • isExecuting: Boolean indicating if the function is currently running.
    • lastResult: The result of the most recent successful execution.
    • rejectionCount: Number of executions rejected due to rate limiting.
    • settleCount: Number of completed executions (success or error).
    • successCount: Number of successful executions.
    // 1. Hook-level subscription via selector
    const asyncRateLimiter = useAsyncRateLimiter(
      async (id) => api.fetch(id),
      { limit: 5, window: 1000 },
      (state) => ({ isExecuting: state.isExecuting })
    );
    
    // 2. Deep subscription via Subscribe HOC
    <asyncRateLimiter.Subscribe selector={(state) => ({ rejectionCount: state.rejectionCount })}>
      {({ rejectionCount }) => <div>Rejected: {rejectionCount}</div>}
    </asyncRateLimiter.Subscribe>
  12. Manage state and lifecycle in asyncRateLimit

    main

    The asyncRateLimit function uses TanStack Store for reactive state management. You can interact with the execution lifecycle using several callbacks:

    • onSuccess: React to successful function execution.
    • onError: React to function execution errors.
    • onSettled: React to function completion (either success or error).
    • onReject: React to executions being blocked by the rate limit.

    Accessing State

    State includes execution times, success/error counts, and current execution status. You can access this via:

    • The underlying AsyncRateLimiter instance's store.state property.
    • Framework-specific hooks (React/Solid) provided by TanStack Pacer adapters.