Состояния запроса: loading, error, success

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

RTK Query опирается на единый поток состояния, который интегрирован в Redux store. Для каждого запроса формируется набор производных значений:

  • isLoading
  • isFetching
  • isSuccess
  • isError
  • status
  • data
  • error

Эти значения отражают текущее состояние запроса и позволяют разделять первичную загрузку, фоновое обновление и завершённые состояния.

Внутренне состояние также связано с жизненным циклом Redux Toolkit Query endpoint:

  • pending
  • fulfilled
  • rejected

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

Состояние loading

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

Ключевой флаг:

  • isLoading === true

Это состояние возникает, когда:

  • запрос выполняется впервые
  • отсутствуют данные в кэше
  • нет активного предыдущего успешного результата

Типичный сценарий:

const { data, isLoading } = useGetUsersQuery();

При первом вызове:

  • data равно undefined
  • isLoading равно true
  • isFetching также true

Особенность RTK Query заключается в том, что loading не используется для повторных запросов, если данные уже существуют в кэше.

Разделение важно:

  • isLoading — первичная загрузка
  • isFetching — любой сетевой запрос (включая повторный)

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

Состояние fetching как расширение loading

Хотя основная тема — loading, важно учитывать связанное состояние isFetching.

const { data, isFetching } = useGetUsersQuery();

isFetching становится true:

  • при первом запросе
  • при refetch
  • при изменении аргументов запроса
  • при автоматическом обновлении кэша

В отличие от isLoading, это состояние не зависит от наличия данных.

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

Ситуация isLoading isFetching
первый запрос true true
данные уже есть, идёт refetch false true
данные получены false false

Это разделение позволяет строить UI без мигания контента при обновлениях.

Состояние success

Состояние успеха определяется флагом:

  • isSuccess === true

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

const { data, isSuccess } = useGetUsersQuery();

Условия перехода в success:

  • получен ответ от сервера
  • отсутствует ошибка
  • данные записаны в cache slice RTK Query

Особенности:

  • isSuccess может оставаться true даже при последующих refetch, если данные валидны
  • повторные запросы не сбрасывают success в false
  • обновление данных происходит без изменения статуса успеха

Важно учитывать, что success не означает отсутствие сетевой активности. Он означает наличие валидного результата в кэше.

Взаимодействие success и data

Состояние success тесно связано с наличием данных:

  • isSuccess === true почти всегда означает data !== undefined

Однако возможны нюансы:

  • при skipToken запрос не выполняется, success не устанавливается
  • при ручном invalidation данные могут быть временно устаревшими, но флаг success сохраняется до перезапроса
const { data, isSuccess } = useGetUserByIdQuery(id);

При корректном выполнении:

  • data содержит нормализованный ответ
  • isSuccess сигнализирует о завершённом успешном lifecycle

Состояние error

Состояние ошибки определяется флагом:

  • isError === true

и сопровождается объектом:

  • error

Ошибка устанавливается при завершении запроса с отклонением:

  • HTTP ошибки (4xx, 5xx)
  • сетевые ошибки
  • кастомные ошибки из baseQuery

Пример:

const { error, isError } = useGetUsersQuery();

Структура error зависит от baseQuery (например, fetchBaseQuery):

{
  status: 404,
  data: { message: "Not found" }
}

или:

{
  error: "FETCH_ERROR",
  message: "Network request failed"
}

Особенности состояния error

  • isError становится true только после завершения запроса
  • при повторном запросе состояние сбрасывается
  • при успешном refetch error исчезает
  • error не влияет на кэш, пока не будет заменён новым успешным ответом

Комбинации состояний

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

loading без data

isLoading: true
data: undefined

Первичная загрузка.

success с data

isSuccess: true
data: [...]

Нормальное состояние после получения ответа.

fetching с data

isFetching: true
isSuccess: true
data: [...]

Фоновое обновление данных без потери UI.

error после success

isError: true
error: {...}
data: previousData

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

Влияние кэширования на состояния

RTK Query использует кэширование по ключу аргументов запроса. Это напрямую влияет на состояние loading.

При повторном вызове:

useGetUsersQuery()

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

  • isLoading будет false
  • isFetching может быть true, если выполняется обновление
  • isSuccess останется true

Это ключевое отличие от классических решений без кэша, где каждый запрос заново устанавливает loading.

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

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

useGetUserQuery(userId)

смена userId вызывает:

  • сброс текущего состояния запроса
  • переход в isLoading === true
  • запуск нового запроса

При этом старые данные могут оставаться доступными до завершения нового запроса в зависимости от конфигурации keepPreviousData.

keepPreviousData и влияние на states

Опция keepPreviousData изменяет поведение состояний:

useGetUsersQuery(page, {
  keepPreviousData: true
});

При включении:

  • старые данные сохраняются в data
  • isLoading не активируется повторно
  • используется isFetching для отражения обновления

Это позволяет избегать “пустого экрана” при смене параметров.

skip и отсутствие состояний

При использовании skip или skipToken запрос не выполняется:

useGetUserQuery(id, { skip: !id });

В этом случае:

  • isLoading === false
  • isFetching === false
  • isSuccess === false
  • isError === false

Состояние считается неинициализированным, так как запрос не был запущен.

Роль status как агрегированного индикатора

RTK Query также предоставляет поле:

  • status

Оно принимает значения:

  • uninitialized
  • pending
  • fulfilled
  • rejected

Это низкоуровневое представление, которое обычно дублируется флагами:

  • isLoading
  • isSuccess
  • isError

Соответствие:

  • pending → loading/fetching
  • fulfilled → success
  • rejected → error

Итоговая модель поведения состояний

Поведение можно представить как последовательность:

  1. uninitialized
  2. pendingisLoading
  3. fulfilledisSuccess
  4. rejectedisError

При повторных запросах:

  • isFetching активируется без сброса success
  • кэш сохраняет предыдущие данные
  • ошибка не удаляет данные, пока не пришёл новый успешный результат

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