← Назад к вопросам
Реализовать пользовательский хук useDebounce
2.2 Middle🔥 201 комментариев
#HTML и CSS#React
Условие
Создайте пользовательский React-хук useDebounce для отложенного обновления значения.
Требования
- Хук принимает значение и задержку в миллисекундах
- Возвращает отложенное (debounced) значение
- Корректно очищает таймер при размонтировании компонента
Пример использования
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 |
| С состоянием | Значения | Да | Loading | Good |
| Callback | Функции | Да | N/A | Специально |
| useRef | Значения | Нет | None | Performant |
Best Practices
- Очистка таймеров — обязательна в return
- Зависимости — включить [value, delay]
- Типизация — использовать generics
- useRef — для хранения timerId
- useCallback — для дебаунсированных функций
Рекомендации для собеседования
- Начните с базового useDebounce (простой и понятный)
- Объясните зависимости и очистку
- Добавьте функцию отмены (бонус)
- Покажите вариант с isWaiting (красиво)
- Упомяните useRef для оптимизации
Лучший выбор для production: Вариант 1 (базовый) или Вариант 2 (с отменой) — просты, эффективны, переиспользуемы.