React Mastery / Custom Hooks

useDebounce Custom Hook

A useDebounce hook is a powerhouse for performance. It prevents a function from firing too often—like when a user is typing in a search bar—by waiting until the user has stopped for a specified amount of time.

Here is a structured breakdown of how to build and use one.


1. The Core Logic

The goal is to delay the update of a value. If the value changes again before the timer finishes, we reset the timer.

The Implementation

import { useState, useEffect } from 'react';

function useDebounce(value, delay) {
  // State to store our debounced value
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    // Set a timer to update debouncedValue after the specified delay
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    // Cleanup: This cancels the timer if the value changes (or component unmounts)
    // This is the "magic" that prevents the update if the user keeps typing
    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]); // Only re-run if value or delay changes

  return debouncedValue;
}

2. Step-by-Step Breakdown

Step

Action

Why?

1. Internal State

Create debouncedValue.

We need a separate "slow" version of the input to return to the component.

2. The Effect

Use useEffect watching value.

Every time the user types, we want to trigger a new countdown.

3. The Timeout

setTimeout inside the effect.

This schedules the "slow" state update for the future (e.g., 500ms).

4. The Cleanup

clearTimeout in the return.

Crucial: If the user types a new character at 200ms, the previous timer is killed before it can finish.


3. How to Use It in a Component

Once you have the hook, you apply it to an input field to prevent unnecessary API calls.

function SearchComponent() {
  const [searchTerm, setSearchTerm] = useState("");
  
  // Use our hook: it only updates after 500ms of "silence"
  const debouncedSearchTerm = useDebounce(searchTerm, 500);

  useEffect(() => {
    if (debouncedSearchTerm) {
      // Logic for API call goes here
      console.log("Searching for:", debouncedSearchTerm);
    }
  }, [debouncedSearchTerm]); // Only runs when debounced value changes

  return (
    <input
      type="text"
      placeholder="Search..."
      onChange={(e) => setSearchTerm(e.target.value)}
    />
  );
}

Key Benefits

  • Reduced API Costs: You aren't hitting your backend for every single keystroke.

  • Better UX: Prevents the UI from flickering or stuttering by trying to process too much data in real-time.

  • Clean Code: Decouples the "input handling" from the "data fetching" logic.

Note: A common mistake is forgetting the cleanup function. Without clearTimeout, every single keystroke will eventually trigger an update, just slightly delayed, which defeats the entire purpose!