Framework
Version
Debouncer API Reference
Throttler API Reference
Rate Limiter API Reference
Queue API Reference
Batcher API Reference

React Example: UseAsyncDebouncedCallback

tsx
import { useState } from 'react'
import ReactDOM from 'react-dom/client'
import { useAsyncDebouncedCallback } from '@tanstack/react-pacer/async-debouncer'

interface SearchResult {
  id: number
  title: string
}

// Simulate API call with fake data
const fakeApi = async (term: string): Promise<Array<SearchResult>> => {
  await new Promise((resolve) => setTimeout(resolve, 500)) // Simulate network delay
  if (term === 'error') {
    throw new Error('Simulated API error')
  }
  return [
    { id: 1, title: `${term} result ${Math.floor(Math.random() * 100)}` },
    { id: 2, title: `${term} result ${Math.floor(Math.random() * 100)}` },
    { id: 3, title: `${term} result ${Math.floor(Math.random() * 100)}` },
  ]
}

function App1() {
  const [searchTerm, setSearchTerm] = useState('')
  const [results, setResults] = useState<Array<SearchResult>>([])
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState<string | null>(null)

  // Create async debounced function - Stable reference provided by useAsyncDebouncedCallback
  const debouncedSearch = useAsyncDebouncedCallback(
    async (term: string) => {
      if (!term.trim()) {
        setResults([])
        return []
      }

      setIsLoading(true)
      setError(null)

      try {
        const data = await fakeApi(term)
        setResults(data)
        return data
      } catch (err) {
        const errorMessage =
          err instanceof Error ? err.message : 'Unknown error'
        setError(errorMessage)
        setResults([])
        throw err
      } finally {
        setIsLoading(false)
      }
    },
    {
      wait: 500,
      // leading: true, // optional, defaults to false
      // trailing: true, // optional, defaults to true
    },
  )

  async function handleSearchChange(e: React.ChangeEvent<HTMLInputElement>) {
    const newValue = e.target.value
    setSearchTerm(newValue)

    try {
      await debouncedSearch(newValue)
    } catch (err) {
      // Error is already handled in the debounced function
      console.log('Search failed:', err)
    }
  }

  return (
    <div>
      <h1>TanStack Pacer useAsyncDebouncedCallback Example 1</h1>
      <div>
        <input
          autoFocus
          type="search"
          value={searchTerm}
          onChange={handleSearchChange}
          placeholder="Type to search... (try 'error' to see error handling)"
          style={{ width: '100%', marginBottom: '10px' }}
        />
      </div>

      {isLoading && <p style={{ color: 'blue' }}>Searching...</p>}
      {error && <p style={{ color: 'red' }}>Error: {error}</p>}

      <div>
        <p>Current search term: {searchTerm}</p>
        {results.length > 0 && (
          <div>
            <h3>Results:</h3>
            <ul>
              {results.map((result) => (
                <li key={result.id}>{result.title}</li>
              ))}
            </ul>
          </div>
        )}
      </div>
    </div>
  )
}

function App2() {
  const [count, setCount] = useState(0)
  const [apiCallCount, setApiCallCount] = useState(0)

  // Simulate API call that returns a value
  const incrementApi = async (value: number): Promise<number> => {
    await new Promise((resolve) => setTimeout(resolve, 300))
    const newCount = value + 1
    setApiCallCount((prev) => prev + 1)
    return newCount
  }

  // Create async debounced increment function
  const debouncedIncrement = useAsyncDebouncedCallback(
    async (currentValue: number) => {
      const result = await incrementApi(currentValue)
      setCount(result)
      return result
    },
    {
      wait: 1000,
      leading: false, // Don't execute immediately
      trailing: true, // Execute after delay
    },
  )

  function handleIncrement() {
    // Update local state immediately for instant feedback
    const newCount = count + 1
    setCount(newCount)

    // Debounced API call
    debouncedIncrement(newCount)
  }

  return (
    <div>
      <h1>TanStack Pacer useAsyncDebouncedCallback Example 2</h1>
      <table>
        <tbody>
          <tr>
            <td>Current Count:</td>
            <td>{count}</td>
          </tr>
          <tr>
            <td>API Calls Made:</td>
            <td>{apiCallCount}</td>
          </tr>
        </tbody>
      </table>
      <div>
        <button onClick={handleIncrement}>
          Increment (debounced API call)
        </button>
      </div>
      <p style={{ fontSize: '0.9em', color: '#666' }}>
        Click rapidly - API calls are debounced to 1 second, but UI updates
        immediately
      </p>
    </div>
  )
}

function App3() {
  const [email, setEmail] = useState('')
  const [validationResult, setValidationResult] = useState<{
    isValid: boolean
    message: string
  } | null>(null)
  const [isValidating, setIsValidating] = useState(false)

  // Simulate email validation API
  const validateEmail = async (
    emailAddress: string,
  ): Promise<{ isValid: boolean; message: string }> => {
    await new Promise((resolve) => setTimeout(resolve, 400))

    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
    const isValid = emailRegex.test(emailAddress)

    return {
      isValid,
      message: isValid
        ? 'Email is valid!'
        : 'Please enter a valid email address',
    }
  }

  // Create debounced validation function
  const debouncedValidateEmail = useAsyncDebouncedCallback(
    async (emailAddress: string) => {
      if (!emailAddress.trim()) {
        setValidationResult(null)
        return null
      }

      setIsValidating(true)

      try {
        const result = await validateEmail(emailAddress)
        setValidationResult(result)
        return result
      } finally {
        setIsValidating(false)
      }
    },
    {
      wait: 750,
      leading: false,
    },
  )

  function handleEmailChange(e: React.ChangeEvent<HTMLInputElement>) {
    const newEmail = e.target.value
    setEmail(newEmail)
    debouncedValidateEmail(newEmail)
  }

  return (
    <div>
      <h1>TanStack Pacer useAsyncDebouncedCallback Example 3</h1>
      <div style={{ marginBottom: '20px' }}>
        <label>
          Email Address:
          <input
            type="email"
            value={email}
            onChange={handleEmailChange}
            placeholder="Enter your email..."
            style={{
              width: '100%',
              marginTop: '5px',
              padding: '8px',
              borderColor:
                validationResult?.isValid === false
                  ? 'red'
                  : validationResult?.isValid === true
                    ? 'green'
                    : 'initial',
            }}
          />
        </label>
      </div>

      {isValidating && <p style={{ color: 'blue' }}>Validating email...</p>}

      {validationResult && (
        <p
          style={{
            color: validationResult.isValid ? 'green' : 'red',
            fontWeight: 'bold',
          }}
        >
          {validationResult.message}
        </p>
      )}

      <p style={{ fontSize: '0.9em', color: '#666' }}>
        Email validation is debounced to 750ms after you stop typing
      </p>
    </div>
  )
}

const root = ReactDOM.createRoot(document.getElementById('root')!)
root.render(
  <div>
    <App1 />
    <hr />
    <App2 />
    <hr />
    <App3 />
  </div>,
)
import { useState } from 'react'
import ReactDOM from 'react-dom/client'
import { useAsyncDebouncedCallback } from '@tanstack/react-pacer/async-debouncer'

interface SearchResult {
  id: number
  title: string
}

// Simulate API call with fake data
const fakeApi = async (term: string): Promise<Array<SearchResult>> => {
  await new Promise((resolve) => setTimeout(resolve, 500)) // Simulate network delay
  if (term === 'error') {
    throw new Error('Simulated API error')
  }
  return [
    { id: 1, title: `${term} result ${Math.floor(Math.random() * 100)}` },
    { id: 2, title: `${term} result ${Math.floor(Math.random() * 100)}` },
    { id: 3, title: `${term} result ${Math.floor(Math.random() * 100)}` },
  ]
}

function App1() {
  const [searchTerm, setSearchTerm] = useState('')
  const [results, setResults] = useState<Array<SearchResult>>([])
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState<string | null>(null)

  // Create async debounced function - Stable reference provided by useAsyncDebouncedCallback
  const debouncedSearch = useAsyncDebouncedCallback(
    async (term: string) => {
      if (!term.trim()) {
        setResults([])
        return []
      }

      setIsLoading(true)
      setError(null)

      try {
        const data = await fakeApi(term)
        setResults(data)
        return data
      } catch (err) {
        const errorMessage =
          err instanceof Error ? err.message : 'Unknown error'
        setError(errorMessage)
        setResults([])
        throw err
      } finally {
        setIsLoading(false)
      }
    },
    {
      wait: 500,
      // leading: true, // optional, defaults to false
      // trailing: true, // optional, defaults to true
    },
  )

  async function handleSearchChange(e: React.ChangeEvent<HTMLInputElement>) {
    const newValue = e.target.value
    setSearchTerm(newValue)

    try {
      await debouncedSearch(newValue)
    } catch (err) {
      // Error is already handled in the debounced function
      console.log('Search failed:', err)
    }
  }

  return (
    <div>
      <h1>TanStack Pacer useAsyncDebouncedCallback Example 1</h1>
      <div>
        <input
          autoFocus
          type="search"
          value={searchTerm}
          onChange={handleSearchChange}
          placeholder="Type to search... (try 'error' to see error handling)"
          style={{ width: '100%', marginBottom: '10px' }}
        />
      </div>

      {isLoading && <p style={{ color: 'blue' }}>Searching...</p>}
      {error && <p style={{ color: 'red' }}>Error: {error}</p>}

      <div>
        <p>Current search term: {searchTerm}</p>
        {results.length > 0 && (
          <div>
            <h3>Results:</h3>
            <ul>
              {results.map((result) => (
                <li key={result.id}>{result.title}</li>
              ))}
            </ul>
          </div>
        )}
      </div>
    </div>
  )
}

function App2() {
  const [count, setCount] = useState(0)
  const [apiCallCount, setApiCallCount] = useState(0)

  // Simulate API call that returns a value
  const incrementApi = async (value: number): Promise<number> => {
    await new Promise((resolve) => setTimeout(resolve, 300))
    const newCount = value + 1
    setApiCallCount((prev) => prev + 1)
    return newCount
  }

  // Create async debounced increment function
  const debouncedIncrement = useAsyncDebouncedCallback(
    async (currentValue: number) => {
      const result = await incrementApi(currentValue)
      setCount(result)
      return result
    },
    {
      wait: 1000,
      leading: false, // Don't execute immediately
      trailing: true, // Execute after delay
    },
  )

  function handleIncrement() {
    // Update local state immediately for instant feedback
    const newCount = count + 1
    setCount(newCount)

    // Debounced API call
    debouncedIncrement(newCount)
  }

  return (
    <div>
      <h1>TanStack Pacer useAsyncDebouncedCallback Example 2</h1>
      <table>
        <tbody>
          <tr>
            <td>Current Count:</td>
            <td>{count}</td>
          </tr>
          <tr>
            <td>API Calls Made:</td>
            <td>{apiCallCount}</td>
          </tr>
        </tbody>
      </table>
      <div>
        <button onClick={handleIncrement}>
          Increment (debounced API call)
        </button>
      </div>
      <p style={{ fontSize: '0.9em', color: '#666' }}>
        Click rapidly - API calls are debounced to 1 second, but UI updates
        immediately
      </p>
    </div>
  )
}

function App3() {
  const [email, setEmail] = useState('')
  const [validationResult, setValidationResult] = useState<{
    isValid: boolean
    message: string
  } | null>(null)
  const [isValidating, setIsValidating] = useState(false)

  // Simulate email validation API
  const validateEmail = async (
    emailAddress: string,
  ): Promise<{ isValid: boolean; message: string }> => {
    await new Promise((resolve) => setTimeout(resolve, 400))

    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
    const isValid = emailRegex.test(emailAddress)

    return {
      isValid,
      message: isValid
        ? 'Email is valid!'
        : 'Please enter a valid email address',
    }
  }

  // Create debounced validation function
  const debouncedValidateEmail = useAsyncDebouncedCallback(
    async (emailAddress: string) => {
      if (!emailAddress.trim()) {
        setValidationResult(null)
        return null
      }

      setIsValidating(true)

      try {
        const result = await validateEmail(emailAddress)
        setValidationResult(result)
        return result
      } finally {
        setIsValidating(false)
      }
    },
    {
      wait: 750,
      leading: false,
    },
  )

  function handleEmailChange(e: React.ChangeEvent<HTMLInputElement>) {
    const newEmail = e.target.value
    setEmail(newEmail)
    debouncedValidateEmail(newEmail)
  }

  return (
    <div>
      <h1>TanStack Pacer useAsyncDebouncedCallback Example 3</h1>
      <div style={{ marginBottom: '20px' }}>
        <label>
          Email Address:
          <input
            type="email"
            value={email}
            onChange={handleEmailChange}
            placeholder="Enter your email..."
            style={{
              width: '100%',
              marginTop: '5px',
              padding: '8px',
              borderColor:
                validationResult?.isValid === false
                  ? 'red'
                  : validationResult?.isValid === true
                    ? 'green'
                    : 'initial',
            }}
          />
        </label>
      </div>

      {isValidating && <p style={{ color: 'blue' }}>Validating email...</p>}

      {validationResult && (
        <p
          style={{
            color: validationResult.isValid ? 'green' : 'red',
            fontWeight: 'bold',
          }}
        >
          {validationResult.message}
        </p>
      )}

      <p style={{ fontSize: '0.9em', color: '#666' }}>
        Email validation is debounced to 750ms after you stop typing
      </p>
    </div>
  )
}

const root = ReactDOM.createRoot(document.getElementById('root')!)
root.render(
  <div>
    <App1 />
    <hr />
    <App2 />
    <hr />
    <App3 />
  </div>,
)
Our Partners
Unkey
Subscribe to Bytes

Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.

Bytes

No spam. Unsubscribe at any time.

Subscribe to Bytes

Your weekly dose of JavaScript news. Delivered every Monday to over 100,000 devs, for free.

Bytes

No spam. Unsubscribe at any time.