If you've ever wired up a search bar in Next.js, you’ve probably hit this problem: every keystroke fires a new API call, triggers a re-render, or runs an expensive calculation. As your app grows, those “harmless little updates” start stacking up—fast.
I’ve been there. I remember watching my terminal fill with logs like a slot machine every time I typed a word. And because I was calling an API, sometimes I even hit rate limits just by typing too quickly.
That’s when I realized: I didn’t need results instantly. I needed results after the user paused typing.
That’s exactly what debouncing solves—and why building a small, reusable useDebounce hook changed the flow of my whole project.
🚨 The Problem: Too Many Updates
React is responsive—which is great—until it isn’t.
Common places this becomes a problem:
- Search bars that fire API calls on every letter
- Window resize listeners
- Live form validation
- Text fields tied to heavy filtering logic
The consequences:
- Wasted API calls (and money)
- Janky UI performance
- Constant re-renders
- Hitting provider rate limits
- Sluggish, frustrating user experience
Here’s a simple example of the problem:
useEffect(() => {
fetch(`/api/search?q=${query}`);
}, [query]);
If a user types “nextjs,” this runs 6 separate times:
n → ne → nex → next → nextj → nextjs
Your server cries.
Your UI stutters.
You quietly wonder if you picked the wrong career.
😅 Naive Solutions (And Why They Fail)
❌ 1. “Let me just add a timeout…”
This is the classic first attempt:
setTimeout(() => {
fetch(...)
}, 300);
It seems fine… until you realize:
- You never cancelled the previous timeout
- Race conditions appear
- Multiple calls still fire, just delayed
- Your component becomes a messy spaghetti bowl
❌ 2. “I’ll track the last value manually”
Trust me: you can do this. You shouldn’t do this.
It leads to bugs, stale values, and callback nightmares.
Debouncing deserves its own tidy, reusable hook.
✅ Enter useDebounce: The Custom Hook That Fixes Everything
Debouncing means:
“Only run this action after the value has stopped changing for X milliseconds.”
That tiny delay (300ms, for example) is enough to:
- Prevent API spamming
- Smooth out re-renders
- Improve UX
- Save money and bandwidth
- Make your search bar feel intentional instead of chaotic
This is exactly the kind of logic that belongs inside a Custom Hook.
🧩 How useDebounce Works (with Clear Code + Comments)
Here’s the entire hook, ready to drop into your project:
import { useState, useEffect } from "react";
/**
* useDebounce — delays updating a value until
* `delay` milliseconds have passed without changes.
*/
export function useDebounce<T>(value: T, delay = 300) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
// Start a timer that updates debouncedValue when delay elapses
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Cancel the timer if value changes before delay ends
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
Why this works:
- The timer resets on every keystroke
- The update only happens after the pause
- Cleanup prevents race conditions
- The hook is fully reusable across components
🔍 Using useDebounce in a Next.js Component
Let’s apply it to a real-world search bar.
"use client";
import { useState, useEffect } from "react";
import { useDebounce } from "@/hooks/useDebounce";
export default function SearchBar() {
const [query, setQuery] = useState("");
// Debounce raw user input by 300ms
const debouncedQuery = useDebounce(query, 300);
useEffect(() => {
if (!debouncedQuery) return;
async function search() {
const res = await fetch(`/api/search?q=${debouncedQuery}`);
const data = await res.json();
console.log("Results:", data);
}
search();
}, [debouncedQuery]);
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
className="border p-2"
/>
);
}
What’s happening behind the scenes?
-
queryupdates instantly → keeps the UI responsive -
debouncedQueryupdates after 300ms → triggers one clean API call - No API spam
- No jittery UI
🌟 Benefits of Using useDebounce
1. Better Performance
No more re-render storms.
2. Fewer API Calls
Your backend (and your wallet) will thank you.
3. Smoother UX
Search results feel stable and intentional.
4. Cleaner Code
Centralized, reusable logic.
5. Reusable Everywhere
Use it for:
- Search bars
- Form validation
- Filtering tables
- Window resize
- Drag events
- Autosaving
🧭 Conclusion
Debouncing is one of those small optimizations that makes a huge difference.
The moment you wrap it in a custom React hook like useDebounce, your app becomes faster, smoother, and easier to maintain.
Once you use it in one component, you’ll find yourself adding it everywhere.
If you’ve built your own version—or used useDebounce in interesting ways—I’d love to hear about it. Drop your thoughts in the comments!
Happy coding! 🚀
Top comments (0)