Getting Started
Blocks
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
"use client"
import { useState } from "react"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { useDebounceFn } from "@/hooks/use-debounce-fn"
export function UseDebounceFnDemo() {
const [searchResults, setSearchResults] = useState("")
const debouncedSearch = useDebounceFn((term: string) => {
setSearchResults(term ? `Search results for: "${term}"` : "")
}, 300)
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
debouncedSearch(value)
}
return (
<div className="w-full max-w-md space-y-4">
<div>
<Label htmlFor="search">Search (debounced)</Label>
<Input
id="search"
onChange={handleInputChange}
placeholder="Type to search..."
/>
</div>
{searchResults && (
<div className="text-muted-foreground text-sm">{searchResults}</div>
)}
</div>
)
}
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
Parameter | Type | Default | Description |
---|---|---|---|
callback | (...args: T) => void | - | The function to debounce |
time | number | 300 | Delay 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.
Build Your SaaS in Days, Not Months
NOW.TS is the Next.js 15 boilerplate with everything you need to launch your SaaS—auth, payments, database, and AI-ready infrastructure.
Learn more about NOW.TS