Ручное управление запросами с помощью useLazyQuery

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

Принцип работы useLazyQuery

Хук useLazyQuery возвращает не результат запроса, а триггер-функцию и объект состояния. Запрос не выполняется автоматически при инициализации компонента. Вместо этого разработчик получает функцию, которую можно вызвать в любой момент.

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

const [trigger, result] = useLazyGetUserQuery();

Где:

  • trigger — функция запуска запроса
  • result — объект состояния запроса (данные, ошибки, статус)

Ключевое отличие от useQuery заключается в отсутствии автоматического выполнения запроса.

Базовый сценарий использования

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

const [fetchUser, { data, error, isFetching }] = useLazyGetUserQuery();

const handleClick = () => {
  fetchUser(1);
};

В данном примере запрос к серверу выполняется только после вызова handleClick. Пока функция не вызвана, запрос не отправляется, а состояние остаётся в начальном виде.

Передача параметров в запрос

Параметры передаются непосредственно в функцию-триггер. Они аналогичны аргументам useQuery.

const [getPost, { data }] = useLazyGetPostQuery();

getPost({ postId: 42 });

В зависимости от определения endpoint, параметры могут быть объектом, строкой или числом. RTK Query использует их для формирования cache key, обеспечивая корректное кэширование результатов.

Состояние запроса

Объект состояния, возвращаемый useLazyQuery, включает все стандартные поля RTK Query:

  • data — результат запроса
  • error — ошибка выполнения
  • isFetching — идёт ли запрос
  • isSuccess — успешное завершение
  • isError — наличие ошибки
  • status — текущий статус (uninitialized, pending, fulfilled, rejected)

Особенность useLazyQuery заключается в начальном состоянии uninitialized, которое означает, что запрос ещё не запускался.

const [loadData, result] = useLazyGetDataQuery();

// result.status === "uninitialized"

После вызова триггера состояние изменяется на pending, а затем на fulfilled или rejected.

Повторные вызовы trigger

Функция trigger может вызываться многократно. Каждый вызов инициирует новый запрос, если параметры изменились или если не используется кэш.

trigger(1);
trigger(2);
trigger(1);

RTK Query применяет стратегию кэширования, поэтому повторные запросы с одинаковыми параметрами могут возвращать данные из кэша без обращения к серверу, в зависимости от конфигурации keepUnusedDataFor и политики refetch.

Использование результата trigger

Функция trigger возвращает Promise, что позволяет работать с результатом запроса напрямую.

const [fetchUser] = useLazyGetUserQuery();

const handleLoad = async () => {
  try {
    const result = await fetchUser(1).unwrap();
    console.log(result);
  } catch (err) {
    console.error(err);
  }
};

Метод unwrap() извлекает полезные данные или выбрасывает ошибку, что упрощает обработку асинхронных операций.

Отличие от useQuery

Основное различие между useQuery и useLazyQuery заключается в стратегии запуска:

  • useQuery — автоматический запуск при рендере
  • useLazyQuery — ручной запуск через trigger

Дополнительно:

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

Пример различия:

// автоматический запрос
const { data } = useGetUserQuery(1);

// ручной запрос
const [getUser, { data }] = useLazyGetUserQuery();

Очистка состояния

RTK Query не сбрасывает состояние автоматически при повторных вызовах trigger. Однако существует возможность сброса через reset в некоторых реализациях или при размонтировании компонента.

const [loadData, result] = useLazyGetDataQuery();

result.reset();

Сброс возвращает состояние к uninitialized.

Интеграция с условиями UI

useLazyQuery часто используется для UI, где данные запрашиваются только при взаимодействии:

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

Пример поиска:

const [search, { data }] = useLazySearchQuery();

const handleSearch = (text) => {
  search(text);
};

Запрос выполняется только после подтверждения, что снижает количество сетевых операций.

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

Каждый вызов trigger рассматривается как независимая операция. RTK Query формирует cache key на основе аргументов, что позволяет:

  • переиспользовать уже загруженные данные
  • избегать дублирующих запросов
  • сохранять консистентность состояния
trigger({ id: 1 });
trigger({ id: 1 }); // может использовать кэш
trigger({ id: 2 }); // новый запрос

Особенности работы с жизненным циклом

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

  • условном отображении компонентов
  • модальных окнах
  • динамических интерфейсах

Запрос остаётся под контролем логики приложения, а не жизненного цикла React-компонента.

Совместное использование с кешированием RTK Query

Несмотря на ручной запуск, useLazyQuery полностью интегрирован в систему RTK Query:

  • результаты сохраняются в cache
  • повторные запросы оптимизируются
  • поддерживается invalidation через tags
const api = createApi({
  endpoints: (builder) => ({
    getUser: builder.query({
      query: (id) => `/user/${id}`,
      providesTags: ['User']
    })
  })
});

После инвалидации тегов следующий вызов trigger получит актуальные данные.

Типичные сценарии архитектуры

useLazyQuery особенно полезен в следующих архитектурных паттернах:

  • разделение UI и загрузки данных
  • event-driven загрузка данных
  • оптимизация количества запросов
  • управление сложными формами с зависимыми полями

Пример формы:

const [loadOptions, { data }] = useLazyGetOptionsQuery();

useEffect(() => {
  if (selectedValue) {
    loadOptions(selectedValue);
  }
}, [selectedValue]);

Поведение при повторном монтировании

Если компонент размонтируется до завершения запроса, RTK Query продолжает выполнение запроса, но результат будет обработан только если компонент ещё подписан на состояние. Это позволяет избегать утечек данных в UI.

Взаимодействие с middleware RTK

useLazyQuery использует тот же middleware слой, что и обычные запросы RTK Query. Это означает:

  • одинаковая логика кеширования
  • одинаковая обработка ошибок
  • единая система дедупликации
  • единые lifecycle events

Разница существует только на уровне триггера выполнения.

Управление частотой вызовов

Поскольку trigger можно вызывать вручную, важно контролировать частоту запросов. Часто используется debounce или throttle:

const [search] = useLazySearchQuery();

const handleInput = (value) => {
  debounce(() => search(value), 300);
};

Это снижает нагрузку на сервер и предотвращает избыточные запросы.

Поведение при ошибках

Ошибки обрабатываются аналогично useQuery, но дополнительно могут быть обработаны через unwrap():

try {
  await trigger(1).unwrap();
} catch (e) {
  console.log('Ошибка загрузки');
}

Состояние isError и объект error обновляются синхронно с результатом запроса.

Использование в сложных сценариях

В реальных приложениях useLazyQuery часто комбинируется с:

  • useMutation
  • useEffect
  • глобальным состоянием (Redux)
  • роутингом

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