Как правильно реализовать дебаунс в React
Что проверяют
Понимаете ли вы, как таймер живёт между рендерами, и умеете ли его чистить. Следом обычно спрашивают, чем дебаунс отличается от троттлинга и от useDeferredValue, и как не получить устаревший ответ сервера поверх нового.
Как отвечать
Дебаунс откладывает вызов, пока события не прекратятся на заданную паузу, например 300 мс после последнего нажатия. Троттлинг, наоборот, пропускает вызов не чаще раза в интервал. Для поиска по вводу нужен дебаунс, для скролла и ресайза чаще троттлинг.
В React два рабочих паттерна:
- дебаунс значения: хук
useDebouncedValue(value, delay). ВuseEffectставитсяconst id = setTimeout(() => setDebounced(value), delay), а cleanup делаетclearTimeout(id). Каждое новое значение отменяет прошлый таймер, и запрос зависит уже отdebounced; - дебаунс колбэка: один экземпляр на весь жизненный цикл компонента, через
useMemoилиuseRefсdebounceиз lodash или своей реализацией. Чтобы не ловить устаревшее замыкание, актуальный колбэк держат вref, а при размонтировании зовутcancel().
Два важных дополнения. Первое: useDeferredValue не заменяет дебаунс. Он понижает приоритет рендера, но фиксированной паузы у него нет, и число сетевых запросов он не сокращает. Второе: дебаунс не спасает от гонки ответов. Если медленный ответ на «rea» пришёл после ответа на «react», на экране окажутся старые данные. Решение: AbortController в cleanup эффекта или библиотека запросов с ключом по дебаунс-значению, например TanStack Query.
Где спотыкаются
Создают debounce(fn, 300) прямо в теле компонента. На каждом рендере это новая функция с новым таймером, и дебаунс не работает: вызовы просто откладываются, а не схлопываются.
Передают в clearTimeout не id таймера, а саму функцию, или забывают cleanup. Таймер срабатывает после ухода со страницы, и запрос уходит в никуда.
Дебаунсят само значение контролируемого input. Поле начинает отставать от ввода. Дебаунсить нужно побочный эффект, запрос или фильтрацию, а value поля обновлять сразу.