DEV Community

ZeeshanAli-0704
ZeeshanAli-0704

Posted on

Debounce in React Functional Component

import React, { useEffect, useState, useRef, useCallback } from "react";

interface TodoItem {
  id: number;
  title: string;
  completed: boolean;
}

const DropDownWithSearch = () => {
  const [selectedOption, setSelectedOption] = useState<string>("");
  const [data, setData] = useState<TodoItem[]>([]);
  const [error, setError] = useState<string>("");
  const [searchTerm, setSearchTerm] = useState<string>("");
  const [filteredData, setFilteredData] = useState<TodoItem[]>([]);
  const [isOpen, setIsOpen] = useState<boolean>(false);
  const dropdownRef = useRef<HTMLDivElement>(null);

  // ---------- FIXED & STABLE DEBOUNCE HOOK ----------
  const useDebounce = (fn: (...args: any[]) => void, delay: number) => {
    const timeoutRef = useRef<any | null>(null);
    return useCallback(
      (...args: any[]) => {
        if (timeoutRef.current) clearTimeout(timeoutRef.current);
        timeoutRef.current = setTimeout(() => fn(...args), delay);
      },
      [fn, delay]
    );
  };
  // ---------------------------------------------------

  // fetch data
  useEffect(() => {
    const fetchPost = async () => {
      try {
        const response = await fetch("https://jsonplaceholder.typicode.com/todos");
        if (!response.ok) {
          setError("Failed to fetch data");
          return;
        }
        const postData: TodoItem[] = await response.json();
        setData(postData);
        setFilteredData(postData);
      } catch (err) {
        setError("An error occurred while fetching data");
      }
    };
    fetchPost();
  }, []);

  // filter list
  useEffect(() => {
    if (searchTerm) {
      const filtered = data.filter((item) =>
        item.title.toLowerCase().includes(searchTerm.toLowerCase())
      );
      setFilteredData(filtered);
    } else {
      setFilteredData(data);
    }
  }, [searchTerm, data]);

  // detect outside click
  useEffect(() => {
    const handleClickOutside = (event: MouseEvent) => {
      if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
        setIsOpen(false);
      }
    };
    document.addEventListener("mousedown", handleClickOutside);
    return () => document.removeEventListener("mousedown", handleClickOutside);
  }, []);

  // triggered after debounce delay
  const onSearchApply = (value: string) => {
    console.log("search applied");
    setSearchTerm(value);
    setIsOpen(true);
  };

  // debounce wrapper
  const onSearchChange = useDebounce((value: string) => {
    onSearchApply(value);
  }, 5000);

  const handleSearchInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setSearchTerm(e?.target.value);
    onSearchChange(e.target.value);
  };

  const onSelectOption = (title: string) => {
    setSelectedOption(title);
    setSearchTerm(title);
    setIsOpen(false);
  };

  const toggleDropdown = () => setIsOpen(!isOpen);

  return (
    <div
      ref={dropdownRef}
      style={{
        padding: "20px",
        width: "350px",
        position: "relative",
        fontFamily: "'Segoe UI', Tahoma, Geneva, Verdana, sans-serif",
      }}
    >
      <h3
        style={{
          marginBottom: "15px",
          fontSize: "18px",
          color: "red",
          fontWeight: 600,
        }}
      >
        Dropdown with Search
      </h3>

      <div
        style={{
          display: "flex",
          alignItems: "center",
          border: `1px solid ${isOpen ? "#007BFF" : "#ced4da"}`,
          borderRadius: "6px",
          padding: "10px",
          cursor: "pointer",
          boxShadow: isOpen ? "0 0 0 2px rgba(0, 123, 255, 0.2)" : "none",
        }}
        onClick={toggleDropdown}
      >
        <input
          type="text"
          placeholder={selectedOption || "Search or select an option..."}
          value={searchTerm}
          onChange={handleSearchInputChange}
          onClick={(e) => {
            e.stopPropagation();
            setIsOpen(true);
          }}
          style={{
            border: "none",
            outline: "none",
            flex: 1,
            padding: "0",
            fontSize: "15px",
          }}
        />
        <span style={{ marginLeft: "10px" }}>{isOpen ? "" : ""}</span>
      </div>

      {isOpen && filteredData.length > 0 && (
        <ul
          style={{
            position: "absolute",
            top: "calc(100% + 8px)",
            left: 0,
            right: 0,
            maxHeight: "250px",
            overflowY: "auto",
            border: "1px solid #ced4da",
            borderRadius: "6px",
            margin: 0,
            padding: 0,
            listStyle: "none",
            zIndex: 1000,
          }}
        >
          {filteredData.map((each) => (
            <li
              key={each.id}
              onClick={() => onSelectOption(each.title)}
              style={{
                padding: "12px 16px",
                cursor: "pointer",
                borderBottom:
                  filteredData[filteredData.length - 1].id === each.id
                    ? "none"
                    : "1px solid #e9ecef",
                backgroundColor:
                  selectedOption === each.title ? "#f1f8ff" : "transparent",
              }}
            >
              {each.title}
            </li>
          ))}
        </ul>
      )}

      {error && (
        <p style={{ color: "#dc3545", marginTop: "12px" }}>{error}</p>
      )}

      {selectedOption && !isOpen && (
        <p style={{ marginTop: "12px", color: "#28a745" }}>
          Selected: {selectedOption}
        </p>
      )}
    </div>
  );
};

export default DropDownWithSearch;

Enter fullscreen mode Exit fullscreen mode

Top comments (0)