Ленивые запросы

Суть ленивых запросов и отличие от обычной модели выполнения

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

Ленивые запросы (lazy queries) представляют противоположную модель поведения: запрос не запускается автоматически, а ожидает явного триггера. Это позволяет полностью контролировать момент получения данных, откладывая сетевой вызов до тех пор, пока он действительно необходим.

Ключевая идея ленивого запроса заключается в следующем:

  • запрос существует в конфигурации
  • но не выполняется до вызова refetch или изменения состояния enabled
  • управление запуском переходит в руки разработчика

Такой подход особенно важен в сценариях, где:

  • данные зависят от пользовательского действия
  • запрос дорогой по ресурсам
  • необходимо избежать лишних сетевых вызовов
  • данные нужны только при выполнении условий

Механизм отключения автоматического запроса через enabled

Основной способ реализации ленивого запроса в TanStack Query — параметр enabled.

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

function Example() {
  const query = useQuery({
    queryKey: ['user'],
    queryFn: fetchUser,
    enabled: false
  })

  return null
}

При enabled: false происходит следующее:

  • queryFn не вызывается при монтировании
  • запрос не переходит в состояние loading автоматически
  • данные остаются undefined, пока не произойдёт ручной запуск

Внутренне TanStack Query продолжает отслеживать queryKey и сохраняет query в кеше, но выполнение откладывается.


Управляемый запуск через refetch

После отключения автоматического выполнения основной механизм запуска — метод refetch.

function Example() {
  const { data, refetch, isFetching } = useQuery({
    queryKey: ['user'],
    queryFn: fetchUser,
    enabled: false
  })

  return (
    <div>
      <button onCl ick={() => refetch()}>Загрузить данные</button>
      {isFetching && <span>Загрузка...</span>}
      {data && <pre>{JSON.stringify(data)}</pre>}
    </div>
  )
}

Поведение refetch:

  • принудительно запускает queryFn
  • игнорирует значение enabled
  • обновляет состояние запроса (isFetching, status)
  • возвращает Promise, что позволяет работать с результатом асинхронно

Важно учитывать, что refetch всегда работает поверх существующего query instance, а не создаёт новый запрос.


Ленивый запуск через изменение enabled

Второй распространённый подход — динамическое управление enabled.

function Example({ userId }) {
  const enabled = Boolean(userId)

  const { data } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
    enabled
  })

  return null
}

Этот механизм отличается от refetch тем, что:

  • запрос автоматически запускается при переходе enabled в true
  • нет необходимости вручную вызывать методы
  • хорошо интегрируется с реактивным состоянием

Фактически это декларативный вариант ленивого запроса, где запуск зависит от условий.


Комбинация enabled и refetch

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

function Example() {
  const [shouldLoad, setShouldLoad] = useState(false)

  const query = useQuery({
    queryKey: ['data'],
    queryFn: fetchData,
    enabled: shouldLoad
  })

  return (
    <div>
      <button onCl ick={() => setShouldLoad(true)}>
        Активировать загрузку
      </button>

      <button onCl ick={() => query.refetch()}>
        Перезагрузить
      </button>
    </div>
  )
}

Такое разделение позволяет:

  • контролировать первичный запуск
  • выполнять повторные запросы независимо от условий
  • разделять «разрешение на загрузку» и «фактическую загрузку»

Ленивые запросы и кэширование

TanStack Query сохраняет данные независимо от того, был ли запрос ленивым или нет. Однако поведение кэша влияет на восприятие ленивого запроса.

Если данные уже есть в кеше:

  • при включении enabled новый запрос может не выполняться сразу
  • данные возвращаются мгновенно
  • фоновое обновление зависит от staleTime

Если данных нет:

  • ленивый запрос остаётся пустым до триггера
  • состояние data будет undefined

Пример:

const query = useQuery({
  queryKey: ['settings'],
  queryFn: fetchSettings,
  enabled: false,
  staleTime: 1000 * 60
})

Даже при последующем включении enabled, поведение будет зависеть от свежести кеша.


Ленивые запросы и зависимые данные

Одно из ключевых применений ленивых запросов — каскадные (dependent) запросы.

const { data: user } = useQuery({
  queryKey: ['user'],
  queryFn: fetchUser
})

const userId = user?.id

const { data: posts } = useQuery({
  queryKey: ['posts', userId],
  queryFn: () => fetchPosts(userId),
  enabled: !!userId
})

Здесь второй запрос становится ленивым до момента появления userId.

Такой подход решает проблему:

  • отсутствующих параметров
  • гонок запросов
  • преждевременного выполнения queryFn

Ленивые запросы и состояние загрузки

Состояния TanStack Query при ленивом запуске отличаются от обычных:

При enabled: false:

  • status остаётся pending
  • isLoading не становится true
  • isFetching не активируется

После refetch:

  • isFetching = true
  • status переходит в loading
  • затем success или error

Это важно учитывать при построении UI, так как привычная логика загрузки может не сработать без явного триггера.


Ошибки и повторные попытки в ленивом режиме

Поведение retry не зависит от того, ленивый запрос или нет.

const query = useQuery({
  queryKey: ['data'],
  queryFn: fetchData,
  enabled: false,
  retry: 2
})

Если refetch был вызван и запрос упал:

  • будет выполнено до 2 повторных попыток
  • каждая попытка инициируется внутренним механизмом Query
  • ленивость влияет только на старт, не на lifecycle запроса

Практический сценарий: поиск по кнопке

Классический пример ленивого запроса — поисковая форма без автозапроса.

function Search() {
  const [query, setQuery] = useState('')
  const [term, setTerm] = useState('')

  const result = useQuery({
    queryKey: ['search', term],
    queryFn: () => searchApi(term),
    enabled: false
  })

  const handleSearch = () => {
    setTerm(query)
    result.refetch()
  }

  return (
    <div>
      <input value={query} onCha nge={(e) => setQuery(e.target.value)} />
      <button onCl ick={handleSearch}>Поиск</button>
    </div>
  )
}

В этой модели:

  • ввод не вызывает запрос
  • запрос выполняется только по действию
  • queryKey фиксирует состояние поиска

Отличие ленивых запросов от conditionally-enabled запросов

Хотя enabled: false и ленивые запросы часто воспринимаются как одно и то же, между ними есть концептуальная разница:

  • enabled — декларативное условие выполнения
  • refetch — императивный запуск

Первый вариант встроен в реактивную модель данных, второй — управляемое действие.

С точки зрения архитектуры:

  • enabled подходит для зависимостей
  • refetch подходит для событий пользователя

Паттерны использования в сложных интерфейсах

В сложных приложениях ленивые запросы применяются в комбинациях:

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

Пример с модальным окном:

function UserModal({ userId, open }) {
  const query = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
    enabled: open && !!userId
  })

  return null
}

Здесь ленивость определяется состоянием интерфейса, а не действиями пользователя напрямую.


Влияние ленивых запросов на архитектуру данных

Использование ленивых запросов меняет структуру взаимодействия с данными:

  • контроль выполнения переносится из слоя данных в слой UI
  • появляется необходимость явно управлять моментом запроса
  • упрощается предотвращение лишних сетевых вызовов
  • повышается предсказуемость поведения при сложных зависимостях

При этом важно избегать чрезмерного использования refetch как универсального механизма, так как это может привести к дублированию логики и усложнению управления состоянием.