Дебаунсинг и троттлинг

Дебаунсинг в контексте запросов TanStack Query применяется для ограничения частоты сетевых обращений при изменяющихся входных данных, чаще всего при поиске, фильтрации или динамическом вводе. Основная цель заключается в том, чтобы отложить выполнение запроса до момента, когда поток изменений стабилизируется, тем самым снижая нагрузку на сервер и предотвращая избыточные запросы.

В классическом сценарии поиска пользователь вводит текст, и каждое нажатие клавиши может триггерить изменение состояния. Без ограничения это приводит к цепочке запросов на каждый символ. TanStack Query сам по себе не навязывает механизм дебаунса, но предоставляет инструменты, с помощью которых подобное поведение реализуется на уровне ключей запросов, состояния и управления включением запроса.

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

import { useState, useEffect } from 'react'
import { useQuery } from '@tanstack/react-query'

function useDebouncedValue(value, delay) {
  const [debounced, setDebounced] = useState(value)

  useEffect(() => {
    const handler = setTimeout(() => {
      setDebounced(value)
    }, delay)

    return () => clearTimeout(handler)
  }, [value, delay])

  return debounced
}

function useSearch(query) {
  const debouncedQuery = useDebouncedValue(query, 400)

  return useQuery({
    queryKey: ['search', debouncedQuery],
    queryFn: async () => {
      const res = await fetch(`/api/search?q=${debouncedQuery}`)
      return res.json()
    },
    enabled: debouncedQuery.length > 0
  })
}

Такой подход обеспечивает стабильность queryKey и предотвращает мгновенные повторные запросы при каждом изменении ввода.

Разделение ответственности между UI и слоем данных

TanStack Query работает наиболее эффективно, когда управление частотой запросов вынесено из queryFn и queryKey. Дебаунсинг в queryFn считается антипаттерном, поскольку приводит к неконтролируемым побочным эффектам и усложняет кэширование.

Правильная модель предполагает:

  • UI слой отвечает за скорость изменения состояния ввода
  • слой хука отвечает за стабилизацию значений
  • TanStack Query отвечает за кэширование, дедупликацию и состояние запроса

Использование enabled для контроля запуска запроса

Флаг enabled часто используется совместно с дебаунсом для предотвращения запросов при промежуточных состояниях:

useQuery({
  queryKey: ['users', debouncedSearch],
  queryFn: fetchUsers,
  enabled: Boolean(debouncedSearch)
})

Дополнительная логика может включать фильтрацию минимальной длины строки или проверку валидности параметров.

Троттлинг и контроль частоты обновлений

В отличие от дебаунса, троттлинг гарантирует выполнение запроса не чаще определённого интервала времени. Это важно в сценариях, где необходимо получать промежуточные результаты, но с ограничением частоты.

Типичный пример — скроллинг, где запросы могут происходить при достижении определённых порогов.

Реализация троттлинга через lodash:

import { useState, useMemo } from 'react'
import { useQuery } from '@tanstack/react-query'
import throttle from 'lodash.throttle'

function useThrottledValue(value, delay) {
  const [throttled, setThrottled] = useState(value)

  const throttledSetter = useMemo(
    () => throttle(setThrottled, delay),
    [delay]
  )

  useEffect(() => {
    throttledSetter(value)
  }, [value, throttledSetter])

  return throttled
}

Далее значение используется в queryKey аналогично дебаунсу.

Дебаунсинг в infinite queries

При работе с бесконечными списками (useInfiniteQuery) дебаунсинг часто применяется к параметрам фильтрации, но не к самой пагинации. Пагинация должна оставаться детерминированной, иначе кэш страниц теряет консистентность.

useInfiniteQuery({
  queryKey: ['products', debouncedFilter],
  queryFn: ({ pageParam = 0 }) =>
    fetch(`/api/products?filter=${debouncedFilter}&page=${pageParam}`),
  getNextPageParam: (lastPage) => lastPage.nextCursor
})

В данном случае изменение фильтра приводит к полной смене ключа, а значит — сбросу кэша страниц.

Влияние staleTime на необходимость дебаунса

Дебаунсинг часто путается с управлением актуальностью данных, но эти механизмы решают разные задачи. staleTime определяет, как долго данные считаются свежими и не требуют повторного запроса при повторном монтировании или фокусе окна.

useQuery({
  queryKey: ['profile', userId],
  queryFn: fetchProfile,
  staleTime: 1000 * 60
})

При корректной настройке staleTime количество запросов может существенно уменьшиться, снижая необходимость агрессивного дебаунса.

Дедупликация запросов и скрытая оптимизация

TanStack Query уже включает встроенную дедупликацию: если несколько компонентов запрашивают одинаковый queryKey, выполняется один запрос. Это снижает потребность в ручном троттлинге в некоторых сценариях.

Однако при динамических ключах (например, ввод пользователя) дедупликация не заменяет дебаунс, поскольку ключи всё равно меняются слишком часто.

Интеграция с внешними механизмами управления потоком

В сложных приложениях дебаунс и троттл часто реализуются через:

  • RxJS (операторы debounceTime, throttleTime)
  • state managers (Zustand middleware, Redux middlewares)
  • браузерные API (requestAnimationFrame для визуальных обновлений)

Пример с RxJS:

search$.pipe(
  debounceTime(300),
  distinctUntilChanged(),
  switchMap(query => fetch(`/api?q=${query}`).then(r => r.json()))
)

В такой архитектуре TanStack Query может использоваться только для кэширования результата, а поток управления запросами остаётся вне его зоны ответственности.

Стабилизация queryKey как центральный принцип

Ключевой аспект корректного применения дебаунса заключается в стабильности queryKey. Любое нестабильное значение приводит к:

  • постоянному созданию новых кэш-записей
  • потере преимуществ дедупликации
  • увеличению сетевой нагрузки
  • фрагментации состояния

Оптимальная модель предполагает, что queryKey изменяется только после завершения логической паузы ввода или завершения операции изменения фильтров.

Комбинация троттлинга и оптимистического поведения UI

В некоторых интерфейсах требуется мгновенная реакция UI при ограничении сетевых вызовов. В таких случаях локальное состояние обновляется без задержки, а запросы синхронизируются через throttled значение.

Это создаёт разрыв между отображаемым состоянием и состоянием сервера, который компенсируется механизмами refetch и invalidationQueries.

const mutation = useMutation({
  mutationFn: updateItem,
  onSuccess: () => {
    queryClient.invalidateQueries(['items'])
  }
})

При троттлинге важно учитывать вероятность конфликтов между локальными изменениями и серверными данными.

Управление запросами в поисковых интерфейсах

Поисковые интерфейсы являются основным кейсом применения дебаунса в TanStack Query. Правильная архитектура обычно включает:

  • локальный input state
  • debounced state
  • queryKey на основе debounced state
  • enabled guard
  • минимальную длину строки
  • кэширование результатов поиска

Такая комбинация позволяет добиться предсказуемого поведения без перегрузки API.

Ошибки при использовании дебаунса с TanStack Query

Наиболее распространённые проблемы:

  • размещение debounce внутри queryFn
  • отсутствие стабилизации queryKey
  • смешивание throttle и debounce без явной логики
  • игнорирование staleTime и gcTime
  • избыточное использование enabled вместо корректной архитектуры состояния

Каждая из этих ошибок приводит к деградации кэширования и увеличению числа сетевых запросов.

Поведение при смене debounce-интервала

Изменение задержки дебаунса во время работы компонента приводит к пересозданию таймеров и потенциальному сдвигу queryKey-обновлений. Это важно учитывать при динамической настройке UX-параметров, особенно в адаптивных интерфейсах.

Стабильность пользовательского опыта напрямую зависит от согласованности между временем задержки и частотой обновления queryKey.