You have a search input. The user is typing fast. You don’t want to fire off an API call on every keystroke. You debounce.

import { useEffect, useState } from "react";

export function useDebouncedValue<T>(value: T, delay = 300): T {
  const [debounced, setDebounced] = useState(value);

  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), delay);
    return () => clearTimeout(id);
  }, [value, delay]);

  return debounced;
}

Usage:

function Search() {
  const [query, setQuery] = useState("");
  const debouncedQuery = useDebouncedValue(query, 400);

  useEffect(() => {
    if (!debouncedQuery) return;
    fetch(`/api/search?q=${debouncedQuery}`).then(/* ... */);
  }, [debouncedQuery]);

  return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}

The user keeps typing → query updates on every keystroke → debouncedQuery only updates 400ms after they stop. The fetch fires once, not 12 times.

The cleanup function in useEffect is doing the heavy lifting — every new keystroke cancels the pending timeout before it fires.