useDebounceFn

PreviousNext

A React hook that debounces function calls to improve performance and prevent excessive API calls.

The useDebounceFn hook is a utility that debounces function calls, preventing them from being executed too frequently. This is particularly useful for search inputs, API calls, or any situation where you want to limit how often a function is called.

Features

  • Performance Optimization: Reduces unnecessary function calls
  • Customizable Delay: Configure the debounce time
  • Type Safe: Full TypeScript support with generic arguments
  • Memory Efficient: Automatically clears timeouts
  • Simple API: Easy to integrate into existing components

Preview

Installation

pnpm dlx shadcn@latest add https://ui.nowts.app/r/use-debounce-fn.json

Usage

Basic search debouncing

import { useState } from "react"
 
import { useDebounceFn } from "@/hooks/use-debounce-fn"
 
export function SearchInput() {
  const [query, setQuery] = useState("")
 
  const debouncedSearch = useDebounceFn(async (searchTerm: string) => {
    if (!searchTerm.trim()) return
 
    // Perform API call
    const results = await fetch(`/api/search?q=${searchTerm}`)
    const data = await results.json()
    console.log(data)
  }, 500)
 
  return (
    <input
      value={query}
      onChange={(e) => {
        setQuery(e.target.value)
        debouncedSearch(e.target.value)
      }}
      placeholder="Search..."
    />
  )
}

Form validation

import { useState } from "react"
 
import { useDebounceFn } from "@/hooks/use-debounce-fn"
 
export function UsernameInput() {
  const [username, setUsername] = useState("")
  const [isValidating, setIsValidating] = useState(false)
  const [isAvailable, setIsAvailable] = useState<boolean | null>(null)
 
  const validateUsername = useDebounceFn(async (name: string) => {
    if (name.length < 3) {
      setIsAvailable(null)
      return
    }
 
    setIsValidating(true)
 
    try {
      const response = await fetch(`/api/validate-username?username=${name}`)
      const { available } = await response.json()
      setIsAvailable(available)
    } catch (error) {
      console.error("Validation failed:", error)
    } finally {
      setIsValidating(false)
    }
  }, 300)
 
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const value = e.target.value
    setUsername(value)
    setIsAvailable(null)
    validateUsername(value)
  }
 
  return (
    <div>
      <input value={username} onChange={handleChange} placeholder="Username" />
      {isValidating && <span>Checking availability...</span>}
      {isAvailable === true && (
        <span className="text-green-600">Available!</span>
      )}
      {isAvailable === false && <span className="text-red-600">Taken</span>}
    </div>
  )
}

Auto-save functionality

import { useState } from "react"
 
import { useDebounceFn } from "@/hooks/use-debounce-fn"
 
export function AutoSaveEditor() {
  const [content, setContent] = useState("")
  const [lastSaved, setLastSaved] = useState<Date | null>(null)
  const [isSaving, setIsSaving] = useState(false)
 
  const debouncedSave = useDebounceFn(async (text: string) => {
    if (!text.trim()) return
 
    setIsSaving(true)
 
    try {
      await fetch("/api/save-draft", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ content: text }),
      })
      setLastSaved(new Date())
    } catch (error) {
      console.error("Save failed:", error)
    } finally {
      setIsSaving(false)
    }
  }, 1000)
 
  const handleContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
    const value = e.target.value
    setContent(value)
    debouncedSave(value)
  }
 
  return (
    <div className="space-y-2">
      <textarea
        value={content}
        onChange={handleContentChange}
        placeholder="Start typing... (auto-saves after 1 second)"
        rows={10}
        className="w-full rounded border p-2"
      />
      <div className="text-sm text-gray-500">
        {isSaving && "Saving..."}
        {lastSaved &&
          !isSaving &&
          `Last saved: ${lastSaved.toLocaleTimeString()}`}
      </div>
    </div>
  )
}

API Reference

Parameters

ParameterTypeDefaultDescription
callback(...args: T) => void-The function to debounce
timenumber300Delay in milliseconds

Returns

Returns a debounced version of the callback function that accepts the same arguments.

TypeScript

The hook is fully typed and preserves the argument types of your callback function:

// String argument
const debouncedSearch = useDebounceFn((query: string) => {
  // search logic
}, 300)
 
// Multiple arguments with different types
const debouncedUpdate = useDebounceFn(
  (
    id: number,
    data: { name: string; email: string },
    options?: { validate: boolean }
  ) => {
    // update logic
  },
  500
)
 
// No arguments
const debouncedRefresh = useDebounceFn(() => {
  // refresh logic
}, 1000)

Best Practices

1. Choose appropriate delay times

  • Search inputs: 300-500ms
  • Auto-save: 1000-2000ms
  • Form validation: 300-500ms
  • Resize/scroll handlers: 100-200ms

2. Handle loading states

Always show feedback to users when debounced operations are in progress:

const [isLoading, setIsLoading] = useState(false)
 
const debouncedFetch = useDebounceFn(async (query: string) => {
  setIsLoading(true)
  try {
    await fetch(`/api/search?q=${query}`)
  } finally {
    setIsLoading(false)
  }
}, 500)

3. Clean up on unmount

The hook automatically clears pending timeouts, but make sure to handle any async operations properly in your cleanup logic.