Что такое дебаунс (debounce) в React и как реализовать его через кастомный хук?

MiddleReact · Frontend·Обновлено 5 сентября 2026
Коротко
Дебаунс — это техника, которая откладывает выполнение функции до тех пор, пока не пройдёт заданная пауза после последнего вызова. В React его обычно реализуют через кастомный хук useDebounce на базе useEffect и useRef, чтобы не дёргать API или не пересчитывать состояние на каждое нажатие клавиши.

Суть дебаунса

Дебаунс (debounce) — приём, который откладывает выполнение функции до момента, когда с последнего вызова прошло определённое время без новых вызовов. Если вызовы продолжают поступать, таймер каждый раз сбрасывается, и функция срабатывает только один раз — после паузы. Это отличает дебаунс от троттлинга (throttle), который гарантирует выполнение не чаще, чем раз в N миллисекунд, независимо от того, продолжаются вызовы или нет.

В React дебаунс чаще всего нужен там, где пользовательский ввод генерирует много событий подряд: поле поиска, автосохранение формы, обработка resize/scroll, валидация в реальном времени. Без дебаунса каждый символ, введённый в поисковую строку, может вызывать сетевой запрос или тяжёлый пересчёт.

Почему не просто lodash.debounce

Можно взять debounce из lodash и обернуть обработчик, но в React есть нюанс: функция-обработчик пересоздаётся на каждом рендере, а вместе с ней и debounce-обёртка, если создавать её прямо в теле компонента. Из-за этого таймер каждый раз обнуляется заново, и дебаунс перестаёт работать как задумано. Поэтому дебаунс-функцию нужно мемоизировать (например, через useMemo или useRef) либо создавать хук, который дебаунсит само значение, а не обработчик.

Хук useDebounce для значения

Самый распространённый и предсказуемый подход — дебаунсить не колбэк, а значение состояния. Хук принимает исходное значение и задержку, а возвращает «отложенную» копию значения, которая обновляется только после паузы.

Логика:

  1. Храним отложенное значение в состоянии.
  2. В useEffect, зависящем от исходного значения, ставим setTimeout, который через delay мс обновит отложенное значение.
  3. В cleanup-функции эффекта очищаем предыдущий таймер — именно это и даёт эффект дребезга: каждое новое изменение отменяет предыдущий отложенный апдейт.

Хук useDebouncedCallback для функций

Иногда нужно дебаунсить не значение, а сам вызов функции (например, отправку запроса на сервер). Тогда таймер хранят в useRef, чтобы он не терялся между рендерами, а саму функцию оборачивают в useCallback, чтобы не создавать новую дебаунс-обёртку на каждый рендер.

Частые ошибки при реализации

  • Забывают чистить таймер в cleanup-функции useEffect — это приводит к утечкам и «призрачным» обновлениям после размонтирования компонента.
  • Создают debounce-функцию заново на каждом рендере без useRef/useMemo, из-за чего таймер никогда не накапливается и дебаунс не срабатывает.
  • Путают дебаунс с троттлингом и применяют не тот подход к задаче (например, для скролла обычно нужен throttle, а не debounce).
  • Забывают учитывать актуальность замыкания (stale closure) — колбэк внутри debounce может держать устаревшие значения props/state, если не использовать актуальные ref или зависимости.
  • Не отменяют «зависший» сетевой запрос при быстрой смене ввода, из-за чего может прийти устаревший ответ и перезаписать актуальные данные (race condition).

Когда дебаунс не нужен

Если обновления происходят редко или их обработка дешёвая, дебаунс добавляет только сложность. Также для UI, где важна мгновенная обратная связь (например, подсветка ошибки при вводе), лучше подходит throttle или комбинация: мгновенный локальный отклик плюс дебаунс только для тяжёлой части (запрос на сервер).

Что хочет услышать интервьюер

Кандидат чётко объясняет разницу между debounce и throttle

Понимание, зачем в React дебаунс выносят в кастомный хук, а не пишут inline

Знание, что таймер нужно хранить в useRef, а очищать — в cleanup useEffect

Упоминание проблемы пересоздания функции на каждом рендере и роли useCallback/useMemo

Практический пример использования: поиск, автосохранение, валидация

Пример: Хук useDebounce для значения

import { useState, useEffect } from 'react';

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

  useEffect(() => {
    // ставим таймер на обновление отложенного значения
    const timerId = setTimeout(() => {
      setDebouncedValue(value);
    }, delay);

    // при новом изменении value отменяем предыдущий таймер
    return () => clearTimeout(timerId);
  }, [value, delay]);

  return debouncedValue;
}

// использование в поиске
function SearchInput() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 400);

  useEffect(() => {
    if (!debouncedQuery) return;
    // запрос уйдёт только через 400мс после последнего ввода
    fetch(`/api/search?q=${debouncedQuery}`);
  }, [debouncedQuery]);

  return <input value={query} onChange={(e) => setQuery(e.target.value)} />;
}

Пример: Хук useDebouncedCallback для функций

import { useRef, useCallback, useEffect } from 'react';

function useDebouncedCallback<Args extends unknown[]>(
  callback: (...args: Args) => void,
  delay: number
) {
  const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
  const callbackRef = useRef(callback);

  // всегда держим актуальную ссылку на колбэк, чтобы избежать stale closure
  useEffect(() => {
    callbackRef.current = callback;
  }, [callback]);

  useEffect(() => {
    return () => {
      if (timerRef.current) clearTimeout(timerRef.current);
    };
  }, []);

  return useCallback(
    (...args: Args) => {
      if (timerRef.current) clearTimeout(timerRef.current);
      timerRef.current = setTimeout(() => {
        callbackRef.current(...args);
      }, delay);
    },
    [delay]
  );
}

Типичные ошибки

Не очищают setTimeout в cleanup-функции useEffect, что приводит к утечкам памяти

Создают debounce-обёртку заново на каждом рендере без useRef или useMemo

Путают debounce и throttle и предлагают неверное решение под задачу

Не учитывают stale closure — колбэк использует устаревшие значения из замыкания

Не обрабатывают race condition при отмене устаревших сетевых запросов

Лучшие курсы по теме

изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Feature-Sliced Design

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.6
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее