Hone logo
Hone
Problems

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 delay has passed, the previous timer is cleared, and a new timer is started.
  • The original callback function should only be executed after the delay has passed without any further calls to the debounced function.
  • The delay can be a dynamic value that changes over time. The hook should adapt to these changes.
  • The callback function might accept arguments. The debounced function should pass any arguments it receives to the original callback when 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 delay values.
  • 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 delay is 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 useDebounce hook must return a function.
  • The hook should not directly depend on useEffect's dependency array for the delay value. It should correctly handle changes to the delay prop.
  • The callback function 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 useRef to 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 delay changes.
  • 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 delay value in a ref within the hook so that useEffect doesn't re-run the entire hook setup just because the delay value changed.
Loading editor...
typescript