Implementing a useDebounce Hook in React
This challenge requires you to create a custom React hook, useDebounce, that delays the execution of a function until a certain amount of time has passed without it being called again. This is a common pattern used to optimize performance in scenarios like input fields where you don't want to trigger an expensive operation on every single keystroke.
Problem Description
You need to implement a TypeScript React hook named useDebounce. This hook should accept two arguments: a callback function (callback) and a delay time in milliseconds (delay). The hook should return a debounced version of the callback function.
The debounced function should behave as follows:
- When the debounced function is called, a timer is started.
- If the debounced function is called again before the
delayhas passed, the previous timer is cleared, and a new timer is started. - The original
callbackfunction should only be executed after thedelayhas passed without any further calls to the debounced function. - The
delaycan be a dynamic value that changes over time. The hook should adapt to these changes. - The
callbackfunction might accept arguments. The debounced function should pass any arguments it receives to the originalcallbackwhen it's eventually executed.
Key Requirements:
- The hook must be written in TypeScript.
- It should handle re-renders and correctly manage timers.
- It should be able to accept dynamic
delayvalues. - It should pass arguments from the debounced function to the original callback.
- The original callback should be invoked with the last set of arguments provided to the debounced function.
Expected Behavior:
Imagine a search input. As the user types, useDebounce will ensure that the search API call is only made after the user pauses typing for a specified duration (e.g., 300ms).
Edge Cases to Consider:
- What happens if the component unmounts before the timer finishes? The timer should be cleared to prevent memory leaks.
- What happens if the
delayis 0 or negative? It should ideally behave as if there's no delay (or the provided value).
Examples
Example 1: Basic Debouncing
import React, { useState } from 'react';
import { useDebounce } from './useDebounce'; // Assume your hook is in this file
function SearchComponent() {
const [searchTerm, setSearchTerm] = useState('');
const [searchResults, setSearchResults] = useState<string[]>([]);
// A mock API call function
const fetchSearchResults = (query: string) => {
console.log(`Fetching results for: ${query}`);
// In a real app, this would be an API call
setTimeout(() => {
setSearchResults([`Result for ${query} 1`, `Result for ${query} 2`]);
}, 500);
};
const debouncedFetch = useDebounce(fetchSearchResults, 500);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
setSearchTerm(value);
debouncedFetch(value); // Call the debounced function
};
return (
<div>
<input
type="text"
value={searchTerm}
onChange={handleChange}
placeholder="Search..."
/>
<ul>
{searchResults.map((result, index) => (
<li key={index}>{result}</li>
))}
</ul>
</div>
);
}
// Usage:
// In your App.tsx or another component:
// <SearchComponent />
/*
Expected Console Output (simulated):
User types 'a' -> No log
User types 'ap' -> No log
User types 'app' -> No log
User types 'appl' -> No log
User pauses for 500ms
Fetching results for: appl
*/
Example 2: Dynamic Delay
import React, { useState } from 'react';
import { useDebounce } from './useDebounce'; // Assume your hook is in this file
function VolumeControl() {
const [volume, setVolume] = useState(50);
const [displayVolume, setDisplayVolume] = useState(50);
// Function to simulate saving volume (e.g., to backend)
const saveVolume = (newVolume: number) => {
console.log(`Saving volume: ${newVolume}`);
// Simulate network delay
setTimeout(() => {
console.log(`Volume ${newVolume} saved.`);
}, 1000);
};
// Debounce the saveVolume function with a delay that can change
const debouncedSaveVolume = useDebounce(saveVolume, 1000); // Initial delay of 1000ms
const handleVolumeChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const newVolume = parseInt(event.target.value, 10);
setVolume(newVolume);
setDisplayVolume(newVolume); // Update display immediately
debouncedSaveVolume(newVolume); // Call debounced function with new volume
};
// Example of changing the delay
const handleDelayChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const newDelay = parseInt(event.target.value, 10);
// This is where the useDebounce hook would ideally pick up the new delay.
// For this example, let's assume you'd pass a state variable for delay to useDebounce
// For simplicity here, we'll just show how the debounced function is called.
// A more complete implementation of useDebounce would take a delay ref or state.
console.log(`Attempting to set delay to ${newDelay}ms`);
// In a real useDebounce, the hook would re-setup the timer based on this new delay.
};
return (
<div>
<h2>Volume Control</h2>
<input
type="range"
min="0"
max="100"
value={volume}
onChange={handleVolumeChange}
/>
<p>Current Volume: {displayVolume}</p>
{/* Example of how you might control delay */}
{/* <label>Debounce Delay (ms): </label>
<input
type="number"
defaultValue={1000}
onChange={handleDelayChange}
placeholder="Set debounce delay"
/> */}
</div>
);
}
// Usage:
// <VolumeControl />
/*
Expected Console Output (simulated, if user slides volume knob quickly):
User slides volume...
(pauses for 1000ms)
Saving volume: 75
(after 1000ms more)
Volume 75 saved.
*/
Constraints
- The
useDebouncehook must return a function. - The hook should not directly depend on
useEffect's dependency array for thedelayvalue. It should correctly handle changes to thedelayprop. - The
callbackfunction should be invoked with the latest arguments provided to the debounced function. - Performance: The hook should be efficient and not cause unnecessary re-renders or resource leaks.
Notes
- Consider using
useRefto store the latest callback and timer ID to ensure you always have access to the most up-to-date values. - Think about how to correctly clear the timer when the component unmounts or when the
delaychanges. - The returned debounced function should have a signature that accepts the same arguments as the original
callback. - For dynamic delays, you'll likely want to store the
delayvalue in a ref within the hook so thatuseEffectdoesn't re-run the entire hook setup just because the delay value changed.