Ever typed into a dropdown search box and watched the whole UI freeze up? That happens because the app tries to filter thousands of items on every single keystroke.

We can fix this by building a custom searchable dropdown that uses a debounce technique to wait until the user stops typing. Here is what we will cover in this guide:

  • Setting up state and TypeScript interfaces for our dropdown items.
  • Writing a custom debounce hook to delay search filtering.
  • Styling a clean dropdown menu and input field with Tailwind CSS.
  • Handling keyboard navigation and outside clicks gracefully.

Setting Up the TypeScript Interfaces and Component Shell

First, we need to define the shape of our data so TypeScript can catch bugs early. We also set up the basic state for the search query and the open or closed state of the dropdown list.

import React, { useState } from 'react';

interface Option {
  id: string;
  label: string;
}

interface SearchableDropdownProps {
  options: Option[];
  value: string;
  onChange: (value: string) => void;
}

export const SearchableDropdown: React.FC = ({ options, value, onChange }) => {
  const [isOpen, setIsOpen] = useState(false);
  const [query, setQuery] = useState('');

  return (
    
       setQuery(e.target.value)}
        onFocus={() => setIsOpen(true)}
      />
      {isOpen && (
        
    {options.map((option) => (
  • { onChange(option.id); setQuery(option.label); setIsOpen(false); }} > {option.label}
  • ))}
)} ); };

This gives us a basic functional dropdown that opens when focused and filters items when we type.

TypeScript development Photo by Mohammad Rahmani on Unsplash

Adding Debounce to Prevent UI Lag

To prevent the app from lagging on every keystroke, we use a technique called debouncing, which simply means waiting a fraction of a second after the user stops typing before running the search. Here is how we add a custom debounce hook to our search value.

import React, { useState, useEffect } from 'react';

function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
}

By passing our search input into this hook, we ensure expensive filtering calculations only run when the user pauses their typing.

Common mistakes

A frequent mistake is forgetting to clear the timeout in the useEffect cleanup function, which can cause memory leaks and unpredictable state updates. Another common issue is failing to handle outside clicks, meaning the dropdown stays open even when the user clicks somewhere else on the page.

Clone this component into your project today and try adding keyboard arrow navigation to make it fully accessible.