Skip to Content
Course content

182: Building a Debounced Search Autocomplete

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Why can't I just trigger the search on every single keystroke?

Technically, you can. But in a production app, that's a recipe for disaster. If a user types "JavaScript" quickly, that's 10 separate API requests hitting your server in about two seconds. Most of those requests are useless because the user hasn't finished their thought yet. You're wasting bandwidth, slowing down the browser's main thread, and potentially hitting rate limits on your API.

Beyond the server load, there's a UX issue: "jitter." If the results update instantly and aggressively, the dropdown list might jump around while the user is still typing, which feels twitchy and unpolished. Debouncing ensures we only act once the user has actually paused.

How does the debounce logic actually "reset" the timer?

The secret is using a closure to keep track of a timer ID. I like to think of it as a "stopwatch" that we keep smashing the reset button on. Every time the user presses a key, we kill the previous timer and start a brand new one. The actual search function only fires if the timer manages to reach zero without being interrupted.

function debounce(func, delay) {
  let timeoutId;
  
  return (...args) => {
    // If a timer is already running, kill it immediately
    if (timeoutId) {
      clearTimeout(timeoutId);
    }

    // Start a new timer
    timeoutId = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}

Notice that we return a function. This is crucial because it allows the returned function to "remember" the timeoutId variable across multiple calls. I usually set the delay to around 300ms—it's the "Goldilocks" zone where it feels responsive but still saves the server from a barrage of requests.

How do I tie this into a real search input and display results?

Once you have the utility function, you wrap your API call logic inside it. You don't want to debounce the event listener itself; you want to debounce the action the listener triggers. Here is how I'd set up a simple autocomplete for a movie database:

const searchInput = document.querySelector('#movie-search');
const resultsContainer = document.querySelector('#results');

// This is the expensive operation we want to limit
async function fetchMovies(query) {
  if (!query) {
    resultsContainer.innerHTML = '';
    return;
  }
  
  console.log(`Fetching results for: ${query}...`);
  const response = await fetch(`https://api.example.com/movies?q=${query}`);
  const data = await response.json();
  
  resultsContainer.innerHTML = data.map(movie => `<li>${movie.title}</li>`).join('');
}

// Wrap the fetch function in our debounce utility
const debouncedSearch = debounce((event) => {
  fetchMovies(event.target.value);
}, 300);

// Attach the debounced version to the event listener
searchInput.addEventListener('input', debouncedSearch);

What about the "out-of-order" results problem?

Here is a pro tip: debouncing solves the frequency of requests, but it doesn't solve latency. Imagine a user types "Apple," pauses (request A sent), then types "Apples" and pauses (request B sent). If request B is faster than request A, request A might arrive after B, overwriting your results with outdated data.

To fix this, I usually use an AbortController. It allows you to literally cancel the previous fetch request the moment a new one is initiated. It's a bit more boilerplate, but it's the only way to guarantee your UI stays in sync with the user's actual input.




📋 Practical Task

Build a Debounced User Directory Search

Create a search interface that filters a list of users. You will need to:

  • Implement a debounce function that accepts a callback and a delay.
  • Create an HTML text input and a <ul> to display results.
  • Simulate an API call using a setTimeout (to mimic network lag) that returns a filtered list of names from a hardcoded array of at least 10 users.
  • Ensure that the "API call" only triggers after the user has stopped typing for 400ms.
  • Log "Fetching data..." to the console every time the debounced function actually executes to verify it isn't firing on every keystroke.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.