Debounce

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

/**
 * Custom hook to debounce a value and provide an abort signal for network requests.
 * @param {any} value - The input value changing rapidly (e.g., search text).
 * @param {number} delay - The debounce delay in milliseconds.
 * @returns {any} The debounced value.
 */
const useDebounce = (value, delay) => {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    // 1. Setup a timer to update our value after the specified delay
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    // 2. Cleanup function: This runs EVERY time 'value' or 'delay' changes.
    // It clears the timeout so the value doesn't update prematurely.
    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]); // Triggers effect whenever input value or delay time changes

  return debouncedValue;
};

function App() {
  const [searchText, setSearchText] = useState('');
  const [results, setResults] = useState([]);
  const [isLoading, setIsLoading] = useState(false);

  // Use our custom debounce hook (500ms delay)
  const debouncedSearchText = useDebounce(searchText, 500);

  // This effect handles the API call and the active cancel signal
  useEffect(() => {
    // If the input is empty, clear results and skip the network call
    if (!debouncedSearchText.trim()) {
      setResults([]);
      return;
    }

    // 1. Create a new AbortController instance for this specific request
    const controller = new AbortController();
    const { signal } = controller;

    const fetchDropdownData = async () => {
      setIsLoading(true);
      try {
        // 2. Pass the abort signal directly into the fetch options
        const response = await fetch(
          `https://github.com{debouncedSearchText}`,
          { signal }
        );
        const data = await response.json();
        setResults(data.items || []);
      } catch (error) {
        // 3. Check if the error was thrown purposefully by our cancel signal
        if (error.name === 'AbortError') {
          console.log('Fetch successfully aborted:', debouncedSearchText);
        } else {
          console.error('Actual API Error:', error);
        }
      } finally {
        // Only turn off loading if the request wasn't aborted
        if (!signal.aborted) {
          setIsLoading(false);
        }
      }
    };

    fetchDropdownData();

    // 4. CLEANUP PHASE: If debouncedSearchText changes again while the fetch 
    // is actively running in the background, React calls this cleanup function.
    // It cancels the network request immediately to save bandwidth.
    return () => {
      controller.abort();
    };
  }, [debouncedSearchText]); // Only fires after the 500ms debounce delay passes

  return (
    <div style={{ padding: '20px', fontFamily: 'sans-serif' }}>
      <h2>Debounced Search with Network Abort</h2>

      <input
        type="text"
        value={searchText}
        placeholder="Type to search GitHub users..."
        onChange={(e) => setSearchText(e.target.value)}
        style={{ padding: '8px', width: '300px', fontSize: '16px' }}
      />

      {isLoading && <p>Loading data from server...</p>}

      <ul style={{ marginTop: '15px' }}>
        {results.slice(0, 5).map((user) => (
          <li key={user.id}>{user.login}</li>
        ))}
      </ul>
    </div>
  );
}

export default App;

How the Cancel Signal Works Here

  1. When you type "a", the debounce timer starts.
  2. 500ms passes, debouncedSearchText becomes "a", and the fetch begins with Signal #1.
  3. If you quickly type "b" while Signal #1 is still waiting for the server, debouncedSearchText updates to "ab".
  4. The API effect restarts, immediately firing controller.abort() on Signal #1 to drop the old network request, and spawns Signal #2 for the new string. [1]

Would you like to add an error banner component to show a message to the user if the actual API network call fails completely?