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

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

Основными состояниями считаются:

  • loading — запрос выполняется;
  • error — запрос завершился ошибкой;
  • success — данные успешно получены.

В TanStack Query эти состояния представлены через набор свойств, которые возвращает хук useQuery.


Базовая структура состояния запроса

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

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

function Users() {
  const query = useQuery({
    queryKey: ['users'],
    queryFn: async () => {
      const response = await fetch('/api/users')

      if (!response.ok) {
        throw new Error('Ошибка загрузки')
      }

      return response.json()
    }
  })

  return null
}

Объект query содержит множество свойств:

{
  data,
  error,
  isLoading,
  isError,
  isSuccess,
  status,
  fetchStatus,
  refetch,
  ...
}

Наиболее важными являются:

Свойство Назначение
isLoading Идёт первая загрузка
isError Запрос завершился ошибкой
isSuccess Данные успешно получены
data Полученные данные
error Объект ошибки
status Текстовое состояние

Состояние loading

Назначение

Состояние loading означает, что запрос ещё не завершён и данные пока отсутствуют.

В этот момент обычно отображаются:

  • спиннеры;
  • skeleton-loader;
  • заглушки интерфейса;
  • текст «Загрузка…».

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

function Users() {
  const {
    data,
    isLoading
  } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers
  })

  if (isLoading) {
    return <div>Загрузка...</div>
  }

  return (
    <ul>
      {data.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
}

Что происходит во время loading

Когда компонент монтируется:

  1. TanStack Query проверяет наличие данных в кеше.

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

    • начинается запрос;
    • isLoading === true;
    • status === 'loading'.
  3. После завершения:

    • состояние меняется на success или error.

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

Это одна из самых важных тем в TanStack Query.

isLoading

Показывает первую загрузку, когда данных ещё нет.

isLoading === true

означает:

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

isFetching

Показывает любой сетевой запрос, включая фоновые обновления.

isFetching === true

может означать:

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

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

const {
  data,
  isLoading,
  isFetching
} = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers
})

Первый рендер

isLoading = true
isFetching = true
data = undefined

После загрузки

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

Фоновое обновление

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

Интерфейс уже имеет данные, но запрос обновляется.


Правильное использование isFetching

function Users() {
  const {
    data,
    isLoading,
    isFetching
  } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers,
    refetchOnWindowFocus: true
  })

  if (isLoading) {
    return <div>Первичная загрузка...</div>
  }

  return (
    <>
      {isFetching && (
        <div>Обновление данных...</div>
      )}

      <ul>
        {data.map(user => (
          <li key={user.id}>
            {user.name}
          </li>
        ))}
      </ul>
    </>
  )
}

Состояние error

Назначение

Состояние error возникает, если запрос завершился неудачно.

Причинами могут быть:

  • сервер недоступен;
  • ошибка сети;
  • HTTP 500;
  • неверный JSON;
  • исключение внутри queryFn.

Генерация ошибки

TanStack Query считает запрос ошибочным только при выбрасывании исключения.

Неправильный пример:

queryFn: async () => {
  const response = await fetch('/api/users')

  return response.json()
}

fetch не выбрасывает ошибку при HTTP 404 или 500.


Правильная обработка

queryFn: async () => {
  const response = await fetch('/api/users')

  if (!response.ok) {
    throw new Error('Ошибка сервера')
  }

  return response.json()
}

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

function Users() {
  const {
    data,
    isError,
    error
  } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers
  })

  if (isError) {
    return (
      <div>
        Ошибка: {error.message}
      </div>
    )
  }

  return (
    <div>{JSON.stringify(data)}</div>
  )
}

Объект error

Свойство error содержит объект исключения.

Обычно это экземпляр Error.

console.log(error)

Пример:

Error: Ошибка сервера

Типизация ошибок

В TypeScript ошибка обычно имеет тип:

unknown

Поэтому часто используется приведение:

if (error instanceof Error) {
  console.log(error.message)
}

Retry-механизм и error

По умолчанию TanStack Query автоматически повторяет запрос при ошибке.

Стандартное значение:

retry: 3

Это означает:

  • первый запрос;
  • ещё 3 автоматические попытки;
  • затем состояние error.

Отключение повторов

useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  retry: false
})

Настройка количества попыток

useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  retry: 5
})

Retry delay

Интервал между попытками:

useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  retryDelay: 1000
})

Состояние success

Назначение

Состояние success означает:

  • данные успешно получены;
  • запрос завершён;
  • data содержит результат.

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

function Users() {
  const {
    data,
    isSuccess
  } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers
  })

  if (isSuccess) {
    return (
      <div>
        Пользователей: {data.length}
      </div>
    )
  }

  return null
}

Что происходит после success

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

  • данные попадают в кеш;
  • компонент получает результат;
  • другие компоненты могут использовать те же данные;
  • запускаются механизмы stale/cache logic.

Свойство status

TanStack Query предоставляет строковое состояние:

status

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

Значение Описание
loading Выполняется загрузка
error Произошла ошибка
success Данные получены

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

const query = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers
})

if (query.status === 'loading') {
  return <div>Загрузка...</div>
}

if (query.status === 'error') {
  return <div>Ошибка</div>
}

return <div>Готово</div>

status vs boolean-флаги

Эти варианты эквивалентны:

query.status === 'loading'

и

query.isLoading

Однако boolean-флаги обычно удобнее.


Комбинирование состояний

Часто используется полный набор проверок:

function Users() {
  const {
    data,
    error,
    isLoading,
    isError
  } = useQuery({
    queryKey: ['users'],
    queryFn: fetchUsers
  })

  if (isLoading) {
    return <div>Загрузка...</div>
  }

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

  return (
    <ul>
      {data.map(user => (
        <li key={user.id}>
          {user.name}
        </li>
      ))}
    </ul>
  )
}

Паттерн раннего возврата

Такой подход называется early return.

Он делает код:

  • проще;
  • чище;
  • легче для чтения;
  • удобнее для поддержки.

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

Иногда загрузку нужно скрыть.

Для этого можно использовать временные данные:

useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  placeholderData: []
})

Теперь:

isLoading === false

поскольку данные уже существуют.


initialData и состояния

initialData также влияет на состояние.

useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  initialData: []
})

Запрос сразу считается успешным:

status === 'success'

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


Fetch status

Помимо status существует fetchStatus.

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

Значение Описание
fetching Выполняется запрос
paused Запрос приостановлен
idle Запрос не выполняется

Отличие status и fetchStatus

status

Отвечает за состояние данных.

fetchStatus

Отвечает за состояние сетевой активности.


Пример

{
  status: 'success',
  fetchStatus: 'fetching'
}

Это означает:

  • данные уже есть;
  • выполняется фоновое обновление.

Состояния при отключённом enabled

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

Состояние:

status = 'loading'
fetchStatus = 'idle'

Запрос ещё не стартовал.


Поведение при refetch

const query = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers
})

query.refetch()

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

isFetching === true

Но:

isLoading === false

Потому что данные уже существуют.


staleTime и состояния

Если данные свежие:

staleTime: 1000 * 60

TanStack Query не будет повторно загружать их при повторном открытии компонента.

Следовательно:

isLoading === false

UX-подходы к состояниям

Плохой UX

Полное скрытие интерфейса при каждом refetch.

if (isLoading || isFetching) {
  return <Spinner />
}

Интерфейс постоянно мигает.


Хороший UX

Разделение первичной загрузки и фонового обновления.

if (isLoading) {
  return <FullPageLoader />
}

И отдельно:

{isFetching && <SmallLoader />}

Обработка пустых данных

После success данные могут быть пустыми.

if (data.length === 0) {
  return <div>Список пуст</div>
}

Это отдельное состояние интерфейса, не связанное с error.


Частые ошибки

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

Неправильно:

if (isFetching) {
  return <Spinner />
}

Интерфейс будет исчезать при каждом обновлении.


Отсутствие throw

Неправильно:

if (!response.ok) {
  return null
}

TanStack Query не узнает об ошибке.


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

const { data } = useQuery(...)

При ошибке компонент может сломаться.


Рекомендуемый шаблон

function Component() {
  const {
    data,
    error,
    isLoading,
    isError,
    isFetching
  } = useQuery({
    queryKey: ['resource'],
    queryFn: fetchResource
  })

  if (isLoading) {
    return <Loader />
  }

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

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

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

Внутренний жизненный цикл состояний

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

1. Монтирование

status = 'loading'
fetchStatus = 'fetching'

2. Успешный ответ

status = 'success'
fetchStatus = 'idle'

3. Фоновое обновление

status = 'success'
fetchStatus = 'fetching'

4. Ошибка обновления

Если данные уже были:

status = 'success'

может сохраниться, несмотря на ошибку refetch.

Это важная особенность TanStack Query: библиотека старается не терять рабочие данные.


Сохранение предыдущих данных

При ошибке фонового обновления:

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

Это значительно улучшает UX по сравнению с традиционными подходами к загрузке данных.