Управление состоянием загрузки

RTK Query автоматически управляет жизненным циклом HTTP-запросов и предоставляет набор готовых флагов состояния, позволяющих отслеживать процесс выполнения запроса, повторную загрузку, ошибки и актуальность данных. Управление состоянием загрузки является одной из ключевых возможностей библиотеки, поскольку избавляет приложение от ручного хранения флагов loading, error, success и промежуточных состояний в Redux Store.

Каждый endpoint RTK Query формирует собственное состояние запроса, которое включает:

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

RTK Query использует внутренний state-механизм, позволяющий синхронизировать UI с состоянием сетевого взаимодействия без дополнительного reducer-кода.


Базовые флаги состояния

Хук запроса возвращает объект со служебными полями:

const {
    data,
    error,
    isLoading,
    isFetching,
    isSuccess,
    isError,
    isUninitialized
} = useGetUsersQuery()

Каждый флаг отражает определённый этап жизненного цикла запроса.


isUninitialized

Флаг isUninitialized показывает, что запрос ещё ни разу не запускался.

Это состояние характерно для:

  • lazy-запросов;
  • запросов с skip;
  • условных запросов.

Пример:

const [fetchUser, result] = useLazyGetUserQuery()

console.log(result.isUninitialized)

До вызова fetchUser() значение будет:

true

После запуска запроса:

false

isLoading

Флаг isLoading указывает на первую загрузку данных.

Он становится true только в момент первоначального выполнения запроса, когда данные ещё отсутствуют в кэше.

Пример:

const { data, isLoading } = useGetPostsQuery()

Использование:

if (isLoading) {
    return <Spinner />
}

Ключевая особенность:

isLoading === true

только если:

  • запрос выполняется;
  • кэшированных данных ещё нет.

После получения данных флаг больше не становится true, даже при повторных запросах.


isFetching

Флаг isFetching показывает, что запрос выполняется прямо сейчас, независимо от наличия данных.

Пример:

const { data, isFetching } = useGetPostsQuery()

Сценарий:

  1. Данные уже загружены.
  2. Пользователь обновляет страницу.
  3. RTK Query выполняет повторный запрос.
  4. isFetching === true
  5. isLoading === false

Это особенно важно для UX.

Пример:

<>
    {isFetching && <SmallLoader />}

    <PostsList posts={data} />
</>

Старые данные продолжают отображаться, пока идёт обновление.


Разница между isLoading и isFetching

isLoading

Отвечает за первую загрузку:

Нет данных + выполняется запрос

isFetching

Отвечает за любую активную сетевую операцию:

Выполняется запрос

Даже если данные уже существуют.


Визуальная схема состояний

Первая загрузка

isLoading   = true
isFetching  = true
data        = undefined

Данные загружены

isLoading   = false
isFetching  = false
data        = [...]

Повторное обновление

isLoading   = false
isFetching  = true
data        = [...]

isSuccess

Флаг isSuccess показывает успешное завершение запроса.

Пример:

const { isSuccess } = useGetUsersQuery()

После успешного ответа:

isSuccess === true

Этот флаг часто используется для:

  • отображения контента;
  • запуска побочных эффектов;
  • проверки успешной синхронизации.

Пример:

if (isSuccess) {
    return <UsersList />
}

isError

Флаг isError становится true, если запрос завершился ошибкой.

Пример:

const { error, isError } = useGetUsersQuery()

Использование:

if (isError) {
    return <ErrorBlock error={error} />
}

Структура объекта error

RTK Query возвращает стандартизированный объект ошибки.

Пример для fetchBaseQuery:

{
    status: 404,
    data: {
        message: 'User not found'
    }
}

Использование:

if (error) {
    return (
        <div>
            {error.status}
        </div>
    )
}

Полный пример обработки состояний

function UsersPage() {
    const {
        data,
        error,
        isLoading,
        isFetching,
        isError
    } = useGetUsersQuery()

    if (isLoading) {
        return <FullPageLoader />
    }

    if (isError) {
        return (
            <ErrorMessage>
                {error.status}
            </ErrorMessage>
        )
    }

    return (
        <>
            {isFetching && (
                <TopProgressBar />
            )}

            <UsersTable users={data} />
        </>
    )
}

Поведение состояния при повторных запросах

RTK Query сохраняет предыдущие данные в кэше.

Поэтому при повторной загрузке:

data

не очищается.

Это предотвращает:

  • мерцание интерфейса;
  • исчезновение контента;
  • скачки layout;
  • повторный рендер пустого состояния.

currentData и data

RTK Query предоставляет два разных поля:

data
currentData

data

Содержит последние доступные данные.

Даже если запрос выполняется повторно.

currentData

Содержит данные только для текущего аргумента запроса.

Пример:

const {
    data,
    currentData
} = useGetUserQuery(userId)

При смене userId:

data         -> старые данные
currentData  -> undefined

Это позволяет избежать отображения устаревшего контента.


Использование currentData

if (!currentData && isFetching) {
    return <Skeleton />
}

Состояния mutation

Mutation-хуки имеют аналогичный набор флагов:

const [
    createUser,
    {
        data,
        error,
        isLoading,
        isSuccess,
        isError
    }
] = useCreateUserMutation()

Жизненный цикл mutation

До запуска

isLoading = false
isSuccess = false
isError   = false

Во время выполнения

isLoading = true

После успеха

isSuccess = true

После ошибки

isError = true

reset у mutation

Mutation предоставляет функцию reset.

Пример:

const [
    login,
    { isSuccess, reset }
] = useLoginMutation()

Сброс состояния:

reset()

После сброса:

isSuccess = false
isError   = false
data      = undefined

Отображение Skeleton Loader

RTK Query отлично сочетается с skeleton-интерфейсами.

Пример:

if (isLoading) {
    return <UsersSkeleton />
}

Частичная загрузка интерфейса

Можно разделять глобальную и локальную загрузку.

Пример:

return (
    <>
        {isFetching && (
            <LinearProgress />
        )}

        <Content data={data} />
    </>
)

Отслеживание refetch

RTK Query автоматически выполняет повторные запросы.

Например:

refetchOnFocus: true

или:

refetchOnReconnect: true

Во время повторного запроса:

isFetching = true

Но:

isLoading = false

Ручной refetch

Каждый query-хук возвращает функцию refetch.

Пример:

const {
    data,
    refetch,
    isFetching
} = useGetPostsQuery()

Использование:

<button onCl ick={refetch}>
    Refresh
</button>

Состояние polling

При использовании polling RTK Query периодически обновляет данные.

Пример:

useGetNotificationsQuery(undefined, {
    pollingInterval: 5000
})

Каждые 5 секунд:

isFetching = true

Загрузка при skip

Параметр skip отключает запрос.

Пример:

const { isUninitialized } = useGetUserQuery(id, {
    skip: !id
})

Пока id отсутствует:

isUninitialized = true

selectFromResult и оптимизация загрузки

RTK Query позволяет выбирать только нужные части состояния.

Пример:

const { isFetching } = useGetUsersQuery(undefined, {
    selectFromResult: ({ isFetching }) => ({
        isFetching
    })
})

Это уменьшает количество ререндеров.


derived booleans

RTK Query формирует флаги автоматически на основе внутреннего статуса:

uninitialized
pending
fulfilled
rejected

На их основе строятся:

  • isLoading
  • isFetching
  • isSuccess
  • isError

Статус запроса

Также доступно поле:

status

Пример:

const { status } = useGetUsersQuery()

Возможные значения:

uninitialized
pending
fulfilled
rejected

Практическая схема UI-состояний

Initial Loading

if (isLoading) {
    return <PageSkeleton />
}

Error State

if (isError) {
    return <ErrorPage />
}

Empty State

if (!data.length) {
    return <EmptyState />
}

Refetch State

{isFetching && <RefreshingIndicator />}

Success State

<UsersTable users={data} />

Асинхронные race condition

RTK Query автоматически предотвращает множество проблем:

  • дублирующиеся запросы;
  • конфликтующие состояния;
  • устаревшие ответы;
  • гонки запросов.

При изменении аргументов старые запросы корректно отслеживаются системой кэширования.


keepUnusedDataFor и состояние загрузки

Настройка:

keepUnusedDataFor

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

Пример:

keepUnusedDataFor: 60

Данные будут храниться 60 секунд после отписки компонента.

Если пользователь вернётся раньше:

isLoading = false

Поскольку данные уже находятся в кэше.


Сброс query-состояния

Можно вручную очищать кэш и состояние.

Пример:

dispatch(
    api.util.resetApiState()
)

После сброса:

isUninitialized = true

Состояния при SSR

Во время server-side rendering RTK Query также сохраняет статусы запросов.

После гидратации:

  • состояние загрузки восстанавливается;
  • кэш сохраняется;
  • повторные запросы минимизируются.

DevTools и отслеживание загрузки

Redux DevTools позволяют наблюдать:

  • pending;
  • fulfilled;
  • rejected;
  • cache entry;
  • refetch;
  • polling;
  • invalidation.

Типичные action:

api/executeQuery/pending
api/executeQuery/fulfilled
api/executeQuery/rejected

Типичная архитектура загрузки

Глобальная загрузка страницы

if (isLoading) {
    return <PageLoader />
}

Локальная загрузка обновления

{isFetching && (
    <MiniSpinner />
)}

Ошибки

if (isError) {
    return <ErrorBanner />
}

Контент

<DataGrid data={data} />

Ошибки при неправильной обработке состояний

Использование только isLoading

Ошибка:

if (isLoading) {
    return <Loader />
}

Проблема:

при refetch индикатор не отображается.


Полное скрытие контента при refetch

Ошибка:

if (isFetching) {
    return <Loader />
}

Проблема:

UI исчезает при каждом обновлении.


Игнорирование currentData

Ошибка:

<UserCard user={data} />

При смене id могут отображаться устаревшие данные.


Рекомендуемая стратегия

Initial Load

Использовать:

isLoading

Background Refresh

Использовать:

isFetching

Ошибки

Использовать:

isError

Проверка успешной загрузки

Использовать:

isSuccess

Условные запросы

Использовать:

isUninitialized