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
- When you type
"a", the debounce timer starts. - 500ms passes,
debouncedSearchTextbecomes"a", and thefetchbegins withSignal #1. - If you quickly type
"b"whileSignal #1is still waiting for the server,debouncedSearchTextupdates to"ab". - The API effect restarts, immediately firing
controller.abort()onSignal #1to drop the old network request, and spawnsSignal #2for 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?