TanStack
Getting Started

TanStack Pacer Preact Adapter

In a Preact application, use the Preact Adapter. Its hooks wrap the core Pacer utilities with lifecycle cleanup and reactive state. The adapter also re-exports everything from the core package, so you can import the plain classes and functions from the same place.

Installation

shell
npm install @tanstack/preact-pacer

Preact hooks

See the Preact Functions Reference for the full list of hooks in the Preact Adapter.

Basic usage

Import a Preact-specific hook from the Preact Adapter.

tsx
import { useDebouncedValue } from '@tanstack/preact-pacer'
import { useState } from 'preact/hooks'

const [instantValue, setInstantValue] = useState(0)
const [debouncedValue, debouncer] = useDebouncedValue(instantValue, {
  wait: 1000,
})

Or import a core Pacer class/function that is re-exported from the Preact Adapter.

tsx
import { debounce, Debouncer } from '@tanstack/preact-pacer' // no need to install the core package separately

Option helpers

Option helpers define shared options with full type checking, so you can declare them once and reuse them across hooks.

Debouncer options

tsx
import { useDebouncer } from '@tanstack/preact-pacer'
import { debouncerOptions } from '@tanstack/pacer'

const commonDebouncerOptions = debouncerOptions({
  wait: 1000,
  leading: false,
  trailing: true,
})

const debouncer = useDebouncer(
  (query: string) => fetchSearchResults(query),
  { ...commonDebouncerOptions, key: 'searchDebouncer' }
)

Async queuer options

tsx
import { useAsyncQueuer } from '@tanstack/preact-pacer'
import { asyncQueuerOptions } from '@tanstack/pacer'

const commonAsyncQueuerOptions = asyncQueuerOptions({
  concurrency: 3,
  addItemsTo: 'back',
})

const queuer = useAsyncQueuer(
  async (item: string) => processItem(item),
  { ...commonAsyncQueuerOptions, key: 'itemQueuer' }
)

Rate limiter options

tsx
import { useRateLimiter } from '@tanstack/preact-pacer'
import { rateLimiterOptions } from '@tanstack/pacer'

const commonRateLimiterOptions = rateLimiterOptions({
  limit: 5,
  window: 60000,
  windowType: 'sliding',
})

const rateLimiter = useRateLimiter(
  (data: string) => sendApiRequest(data),
  { ...commonRateLimiterOptions, key: 'apiRateLimiter' }
)

Provider

The PacerProvider component sets default options for every Pacer utility instance in its component tree.

tsx
import { PacerProvider } from '@tanstack/preact-pacer'

// Set default options for preact-pacer instances
<PacerProvider
  defaultOptions={{
    debouncer: { wait: 1000 },
    asyncQueuer: { concurrency: 3 },
    rateLimiter: { limit: 5, window: 60000 },
  }}
>
  <App />
</PacerProvider>

Hooks inside the provider use these defaults. Options passed to an individual hook override them.

Subscribing to state

The Preact Adapter supports subscribing to state changes in two ways:

Using the Subscribe component

Use the Subscribe component to read state deep in the component tree without passing a selector to the hook.

tsx
import { useRateLimiter } from '@tanstack/preact-pacer'

function ApiComponent() {
  const rateLimiter = useRateLimiter(
    (data: string) => {
      return fetch('/api/endpoint', {
        method: 'POST',
        body: JSON.stringify({ data }),
      })
    },
    { limit: 5, window: 60000 }
  )

  return (
    <div>
      <button onClick={() => rateLimiter.maybeExecute('some data')}>
        Submit
      </button>
      
      <rateLimiter.Subscribe selector={(state) => ({ rejectionCount: state.rejectionCount })}>
        {({ rejectionCount }) => (
          <div>Rejections: {rejectionCount}</div>
        )}
      </rateLimiter.Subscribe>
    </div>
  )
}

Using the selector parameter

The selector parameter controls which state changes trigger reactive updates. State you do not select never causes an update.

Without a selector, hook.state is an empty object ({}). Pass a selector function to opt in to state tracking.

tsx
import { useDebouncer } from '@tanstack/preact-pacer'

function SearchComponent() {
  // Default behavior - no reactive state subscriptions
  const untrackedDebouncer = useDebouncer(
    (query: string) => fetchSearchResults(query),
    { wait: 500 }
  )
  console.log(untrackedDebouncer.state) // {}

  // Opt-in to track isPending changes
  const debouncer = useDebouncer(
    (query: string) => fetchSearchResults(query),
    { wait: 500 },
    (state) => ({ isPending: state.isPending })
  )
  console.log(debouncer.state.isPending) // Reactive value

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

For more details on state management and available state properties, see the individual guide pages for each utility (e.g., Rate Limiting Guide, Debouncing Guide).

Examples

Debouncer example

tsx
import { useDebouncer } from '@tanstack/preact-pacer'

function SearchComponent() {
  const debouncer = useDebouncer(
    (query: string) => {
      console.log('Searching for:', query)
      // Perform search
    },
    { wait: 500 }
  )

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

Async queuer example

tsx
import { useAsyncQueuer } from '@tanstack/preact-pacer'

function UploadComponent() {
  const queuer = useAsyncQueuer(
    async (file: File) => {
      await uploadFile(file)
    },
    { concurrency: 3 }
  )

  const handleFileSelect = (files: FileList) => {
    Array.from(files).forEach((file) => {
      queuer.addItem(file)
    })
  }

  return (
    <input
      type="file"
      multiple
      onChange={(e) => {
        if (e.target.files) {
          handleFileSelect(e.target.files)
        }
      }}
    />
  )
}

Rate limiter example

tsx
import { useRateLimiter } from '@tanstack/preact-pacer'

function ApiComponent() {
  const rateLimiter = useRateLimiter(
    (data: string) => {
      return fetch('/api/endpoint', {
        method: 'POST',
        body: JSON.stringify({ data }),
      })
    },
    {
      limit: 5,
      window: 60000,
      windowType: 'sliding',
      onReject: () => {
        alert('Rate limit reached. Please try again later.')
      },
    }
  )

  const handleSubmit = () => {
    const remaining = rateLimiter.getRemainingInWindow()
    if (remaining > 0) {
      rateLimiter.maybeExecute('some data')
    }
  }

  return <button onClick={handleSubmit}>Submit</button>
}