← Назад к вопросам

Реализовать пользовательский хук useDebounce

2.2 Middle🔥 201 комментариев
#HTML и CSS#React

Условие

Создайте пользовательский React-хук useDebounce для отложенного обновления значения.

Требования

  1. Хук принимает значение и задержку в миллисекундах
  2. Возвращает отложенное (debounced) значение
  3. Корректно очищает таймер при размонтировании компонента

Пример использования

function SearchComponent() {
  const [searchTerm, setSearchTerm] = useState("");
  const debouncedSearch = useDebounce(searchTerm, 500);

  useEffect(() => {
    if (debouncedSearch) {
      // Выполнить поиск через API
      searchAPI(debouncedSearch);
    }
  }, [debouncedSearch]);

  return (
    <input
      value={searchTerm}
      onChange={(e) => setSearchTerm(e.target.value)}
      placeholder="Search..."
    />
  );
}

Бонус

Добавьте возможность отмены текущего debounce.

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение

Задача на создание хука useDebounce — классическая для показа понимания асинхронности, эффектов и очистки ресурсов в React. Создадим несколько версий.

Решение 1: Базовый useDebounce

Простая и понятная реализация:

import { useState, useEffect } from "react";

function useDebounce<T>(value: T, delay: number): T {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);

  useEffect(() => {
    // Устанавливаем таймер
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    // Очищаем таймер при изменении value или размонтировании
    return () => {
      clearTimeout(handler);
    };
  }, [value, delay]);

  return debouncedValue;
}

export default useDebounce;

Анализ:

  • useState — хранит отложенное значение
  • useEffect — запускает таймер при изменении value
  • Функция очистки (return () => clearTimeout()) — отменяет таймер
  • Зависимости [value, delay] — пересчитывается при изменении
  • Типизация через generics <T>

Использование базового хука

function SearchComponent() {
  const [searchTerm, setSearchTerm] = useState("");
  const debouncedSearch = useDebounce(searchTerm, 500);

  useEffect(() => {
    if (debouncedSearch) {
      console.log("Выполняем поиск:", debouncedSearch);
      // searchAPI(debouncedSearch);
    }
  }, [debouncedSearch]);

  return (
    <input
      type="text"
      value={searchTerm}
      onChange={(e) => setSearchTerm(e.target.value)}
      placeholder="Search..."
      className="w-full px-4 py-2 border rounded-lg"
    />
  );
}

Решение 2: С возможностью отмены (бонус)

Расширенная версия с функцией отмены:

interface UseDebouncedValueReturn<T> {
  value: T;
  cancel: () => void;
}

function useDebouncedValue<T>(
  value: T,
  delay: number
): UseDebouncedValueReturn<T> {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);
  const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout | null>(null);

  useEffect(() => {
    // Очищаем предыдущий таймер
    if (timeoutId) {
      clearTimeout(timeoutId);
    }

    // Устанавливаем новый таймер
    const handler = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    setTimeoutId(handler);

    // Очистка при размонтировании
    return () => {
      if (handler) clearTimeout(handler);
    };
  }, [value, delay]);

  // Функция отмены
  const cancel = () => {
    if (timeoutId) {
      clearTimeout(timeoutId);
      setTimeoutId(null);
    }
  };

  return { value: debouncedValue, cancel };
}

Использование:

function SearchWithCancel() {
  const [searchTerm, setSearchTerm] = useState("");
  const { value: debouncedSearch, cancel } = useDebouncedValue(searchTerm, 500);

  const handleCancel = () => {
    cancel();
    setSearchTerm("");
  };

  return (
    <div>
      <input
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="Search..."
      />
      <button onClick={handleCancel}>Отмена</button>
    </div>
  );
}

Решение 3: С отслеживанием состояния

Версия, которая показывает, идёт ли debounce в процессе:

interface UseDebouncedStateReturn<T> {
  value: T;
  isWaiting: boolean;
  cancel: () => void;
}

function useDebouncedState<T>(
  value: T,
  delay: number
): UseDebouncedStateReturn<T> {
  const [debouncedValue, setDebouncedValue] = useState<T>(value);
  const [isWaiting, setIsWaiting] = useState(false);
  const timeoutRef = React.useRef<NodeJS.Timeout | null>(null);

  useEffect(() => {
    // Очищаем предыдущий таймер
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
    }

    // Показываем, что ждём
    setIsWaiting(true);

    // Устанавливаем новый таймер
    timeoutRef.current = setTimeout(() => {
      setDebouncedValue(value);
      setIsWaiting(false);
    }, delay);

    // Очистка
    return () => {
      if (timeoutRef.current) clearTimeout(timeoutRef.current);
    };
  }, [value, delay]);

  const cancel = () => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
      timeoutRef.current = null;
    }
    setIsWaiting(false);
  };

  return { value: debouncedValue, isWaiting, cancel };
}

// Использование
function SearchWithIndicator() {
  const [searchTerm, setSearchTerm] = useState("");
  const { value: debouncedSearch, isWaiting } = useDebouncedState(searchTerm, 500);

  return (
    <div>
      <input
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
      />
      {isWaiting && <span className="text-gray-500">Ожидание...</span>}
      {debouncedSearch && <p>Результаты для: {debouncedSearch}</p>}
    </div>
  );
}

Решение 4: Универсальный debounce хук

Для любых функций, а не только значений:

function useDebounceCallback<T extends (...args: any[]) => any>(
  callback: T,
  delay: number
): [T, () => void] {
  const timeoutRef = React.useRef<NodeJS.Timeout | null>(null);

  const debouncedCallback = React.useCallback(
    ((...args) => {
      if (timeoutRef.current) {
        clearTimeout(timeoutRef.current);
      }

      timeoutRef.current = setTimeout(() => {
        callback(...args);
      }, delay);
    }) as T,
    [callback, delay]
  );

  const cancel = () => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
    }
  };

  useEffect(() => {
    return () => cancel();
  }, []);

  return [debouncedCallback, cancel];
}

// Использование
function FormWithDebounceAPI() {
  const [email, setEmail] = useState("");
  const [isChecking, setIsChecking] = useState(false);

  const checkEmail = (emailAddress: string) => {
    setIsChecking(true);
    // API запрос
    console.log("Проверяем email:", emailAddress);
    setIsChecking(false);
  };

  const [debouncedCheck, cancelCheck] = useDebounceCallback(checkEmail, 500);

  return (
    <div>
      <input
        value={email}
        onChange={(e) => {
          setEmail(e.target.value);
          debouncedCheck(e.target.value);
        }}
      />
      {isChecking && <span>Проверка...</span>}
    </div>
  );
}

Решение 5: С useRef вместо useState (более производительно)

Для простого debounce без обновления компонента при каждом вызове:

function useDebounceRef<T>(
  value: T,
  delay: number
): React.MutableRefObject<T> {
  const debouncedRef = React.useRef<T>(value);
  const timeoutRef = React.useRef<NodeJS.Timeout | null>(null);

  useEffect(() => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
    }

    timeoutRef.current = setTimeout(() => {
      debouncedRef.current = value;
    }, delay);

    return () => {
      if (timeoutRef.current) clearTimeout(timeoutRef.current);
    };
  }, [value, delay]);

  return debouncedRef;
}

Сравнение подходов

ВариантИспользованиеОтменаСостояниеРекомендация
БазовыйЗначенияНетПростое✅ Best
С отменойЗначенияДаПростое✅ Best
С состояниемЗначенияДаLoadingGood
CallbackФункцииДаN/AСпециально
useRefЗначенияНетNonePerformant

Best Practices

  1. Очистка таймеров — обязательна в return
  2. Зависимости — включить [value, delay]
  3. Типизация — использовать generics
  4. useRef — для хранения timerId
  5. useCallback — для дебаунсированных функций

Рекомендации для собеседования

  1. Начните с базового useDebounce (простой и понятный)
  2. Объясните зависимости и очистку
  3. Добавьте функцию отмены (бонус)
  4. Покажите вариант с isWaiting (красиво)
  5. Упомяните useRef для оптимизации

Лучший выбор для production: Вариант 1 (базовый) или Вариант 2 (с отменой) — просты, эффективны, переиспользуемы.

Реализовать пользовательский хук useDebounce | PrepBro