Conditional fetching

По умолчанию RTK Query автоматически выполняет запрос сразу после вызова query-хука. Если компонент использует useGetUsersQuery(), запрос будет отправлен немедленно после рендера.

Во многих сценариях такое поведение неудобно:

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

Для решения подобных задач RTK Query предоставляет механизм условного выполнения запросов — conditional fetching.


Автоматическое выполнение query

Стандартный query-хук выглядит следующим образом:

const { data, isLoading } = useGetPostsQuery()

После рендера компонента RTK Query:

  1. создаёт подписку;
  2. проверяет наличие кеша;
  3. при необходимости отправляет HTTP-запрос;
  4. сохраняет результат в store.

Если query принимает аргументы:

const { data } = useGetUserQuery(userId)

RTK Query автоматически отслеживает изменение userId.

При изменении аргумента:

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

Проблема автоматического запроса

Предположим, идентификатор пользователя ещё неизвестен:

const userId = undefined

const { data } = useGetUserQuery(userId)

В этом случае запрос может отправиться с некорректным URL:

/users/undefined

Либо сервер вернёт ошибку:

400 Bad Request

Ещё один распространённый сценарий — поиск:

const [search, setSearch] = useState('')

const { data } = useSearchQuery(search)

Если пользователь ничего не ввёл, приложение начнёт выполнять бессмысленные запросы.


Параметр skip

Основной механизм conditional fetching — параметр skip.

Базовый синтаксис

const { data } = useGetUserQuery(userId, {
  skip: true
})

При skip: true:

  • запрос не выполняется;
  • подписка не создаётся;
  • кеш не используется;
  • статус остаётся uninitialized.

Динамический skip

Наиболее частый вариант:

const { data, isLoading } = useGetUserQuery(userId, {
  skip: !userId
})

Логика:

  • если userId существует — запрос выполняется;
  • если userId отсутствует — запрос пропускается.

Состояние uninitialized

Когда запрос пропущен через skip, RTK Query возвращает специальное состояние:

const result = useGetUserQuery(userId, {
  skip: !userId
})

console.log(result)

Структура:

{
  data: undefined,
  error: undefined,
  isLoading: false,
  isFetching: false,
  isSuccess: false,
  isError: false,
  isUninitialized: true
}

isUninitialized особенно полезен для интерфейсов.

Пример:

if (isUninitialized) {
  return <div>Введите ID пользователя</div>
}

Пример conditional fetching с формой

Без skip

const [email, setEmail] = useState('')

const { data } = useFindUserQuery(email)

Проблемы:

  • запрос выполняется при каждом символе;
  • выполняется запрос с пустой строкой;
  • появляются лишние HTTP-вызовы.

Со skip

const [email, setEmail] = useState('')

const { data } = useFindUserQuery(email, {
  skip: email.length < 3
})

Теперь запрос выполнится только после ввода минимум трёх символов.


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

RTK Query предоставляет специальный объект skipToken.

Импорт:

import { skipToken } from '@reduxjs/toolkit/query'

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

const { data } = useGetUserQuery(
  userId ?? skipToken
)

Отличия skipToken от skip

Вариант со skip

useGetUserQuery(userId, {
  skip: !userId
})

Вариант со skipToken

useGetUserQuery(userId ?? skipToken)

skipToken считается более чистым и безопасным вариантом.

Особенно в TypeScript.


Почему skipToken полезнее

Проблема:

useGetUserQuery(undefined)

Даже при наличии skip TypeScript может считать undefined недопустимым аргументом.

skipToken решает проблему типизации:

useGetUserQuery(userId ?? skipToken)

Теперь аргумент либо валиден, либо RTK Query полностью пропускает запрос.


Пример с авторизацией

Загрузка профиля только после логина

const token = useSelector(state => state.auth.token)

const { data: profile } = useGetProfileQuery(undefined, {
  skip: !token
})

Если токен отсутствует:

  • запрос не отправляется;
  • сервер не получает невалидный Authorization header.

Conditional fetching с несколькими условиями

skip может содержать любую логику.

Пример

const shouldSkip =
  !token ||
  !userId ||
  isModalClosed

const { data } = useGetUserQuery(userId, {
  skip: shouldSkip
})

Отложенная загрузка данных

Иногда запрос не должен выполняться автоматически вообще.

Например:

  • по кнопке;
  • при наведении;
  • после подтверждения;
  • при открытии вкладки.

Для этого используется lazy query.


Lazy queries

RTK Query генерирует специальные lazy hooks.

Обычный query

const { data } = useGetUsersQuery()

Lazy query

const [trigger, result] = useLazyGetUsersQuery()

Как работает lazy query

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

Вызов происходит вручную:

const [getUsers, { data, isLoading }] =
  useLazyGetUsersQuery()

const handleClick = () => {
  getUsers()
}

Пример загрузки по кнопке

function UsersPage() {
  const [
    fetchUsers,
    { data, isLoading }
  ] = useLazyGetUsersQuery()

  return (
    <div>
      <button onCl ick={() => fetchUsers()}>
        Загрузить
      </button>

      {isLoading && <p>Загрузка...</p>}

      {data?.map(user => (
        <div key={user.id}>
          {user.name}
        </div>
      ))}
    </div>
  )
}

Передача аргументов в lazy query

const [getUser, result] =
  useLazyGetUserQuery()

const handleSelect = id => {
  getUser(id)
}

Promise в lazy query

trigger возвращает Promise.

const [getUser] = useLazyGetUserQuery()

const handleLoad = async () => {
  const result = await getUser(15)

  console.log(result)
}

unwrap в lazy query

Как и mutation, lazy query поддерживает unwrap.

const [getUser] = useLazyGetUserQuery()

try {
  const user = await getUser(5).unwrap()

  console.log(user)
} catch (error) {
  console.log(error)
}

unwrap():

  • возвращает только payload;
  • выбрасывает ошибку через throw;
  • упрощает async/await-код.

Conditional fetching при открытии модального окна

const { data } = useGetUserQuery(userId, {
  skip: !isModalOpen
})

Запрос выполняется только после открытия модального окна.


Conditional fetching с tabs

const { data: users } =
  useGetUsersQuery(undefined, {
    skip: activeTab !== 'users'
  })

Conditional fetching при наличии кеша

Иногда данные уже есть в store.

Можно избегать повторной загрузки:

const cachedUser = useSelector(
  state => state.users.entities[id]
)

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

Условная загрузка зависимых данных

Частый сценарий — цепочка запросов.

Например:

  1. загрузка пользователя;
  2. загрузка его постов.

Пример

const { data: user } =
  useGetUserQuery(userId)

const { data: posts } =
  useGetPostsByUserQuery(user?.id, {
    skip: !user
  })

Второй запрос начнётся только после успешной загрузки пользователя.


Conditional fetching и polling

Polling можно отключать динамически.

const { data } = useGetNotificationsQuery(undefined, {
  pollingInterval: isFocused ? 5000 : 0
})

Либо полностью пропускать запрос:

const { data } = useGetNotificationsQuery(undefined, {
  skip: !isFocused
})

Разница между skip и pollingInterval: 0

pollingInterval: 0

pollingInterval: 0
  • запрос всё ещё существует;
  • кеш используется;
  • подписка активна.

skip: true

skip: true
  • query полностью отключён;
  • подписка отсутствует;
  • кеш не отслеживается.

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

Важно понимать:

const { data } = useGetUsersQuery(undefined, {
  skip: true
})

RTK Query не удаляет существующий кеш.

Он просто:

  • не подписывается на него;
  • не выполняет запрос;
  • не отслеживает обновления.

Если позже skip станет false, RTK Query повторно подключится к кешу.


Переключение skip во время работы

const { data } = useGetUsersQuery(undefined, {
  skip: disabled
})

Когда disabled меняется:

false → true

RTK Query:

  • удаляет подписку;
  • прекращает отслеживание.

true → false

RTK Query:

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

refetch и skip

Если запрос пропущен:

const { refetch } = useGetUsersQuery(undefined, {
  skip: true
})

refetch() работать не будет, потому что query не инициализирован.


Правильный способ ручного запуска

Для ручного запуска следует использовать lazy query:

const [fetchUsers] =
  useLazyGetUsersQuery()

Conditional fetching и SSR

При серверном рендеринге conditional fetching особенно важен.

Например:

const { data } = useGetProfileQuery(undefined, {
  skip: typeof window === 'undefined'
})

Это предотвращает выполнение browser-only запросов на сервере.


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

Использование skip вместо lazy query

Ошибка:

const [enabled, setEnabled] = useState(false)

const { refetch } = useGetUsersQuery(undefined, {
  skip: !enabled
})

const handleClick = () => {
  refetch()
}

Проблема:

  • query не инициализирован;
  • refetch() не работает.

Правильнее:

const [fetchUsers] =
  useLazyGetUsersQuery()

fetchUsers()

Проверка только на undefined

Ошибка:

skip: userId === undefined

Если userId = null, запрос всё равно выполнится.

Безопаснее:

skip: !userId

Либо:

userId ?? skipToken

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

Ошибка:

useSearchQuery(search)

Исправление:

useSearchQuery(search, {
  skip: search.length < 2
})

Комбинирование conditional fetching и selectFromResult

const result = useGetUsersQuery(undefined, {
  skip: !enabled,
  selectFromResult: ({ data }) => ({
    users: data ?? []
  })
})

Это позволяет:

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

Conditional fetching в реальных приложениях

Наиболее распространённые сценарии:

Сценарий Решение
Нет ID skip
Нет токена skip
Загрузка по кнопке lazy query
Поиск skip
Зависимые запросы skip
Tabs skip
Модальные окна skip
SSR skip
TypeScript-safe skip skipToken

Архитектурные рекомендации

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

skip: !requiredData

Подходит для:

  • ID;
  • токенов;
  • флагов интерфейса;
  • параметров фильтрации.

Использование lazy query для действий пользователя

useLazyQuery()

Подходит для:

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

Использование skipToken в TypeScript-проектах

id ?? skipToken

Наиболее безопасный вариант типизации.


Полный пример conditional fetching

import {
  skipToken
} from '@reduxjs/toolkit/query'

function UserProfile({ userId }) {
  const token = useSelector(
    state => state.auth.token
  )

  const shouldSkip =
    !token || !userId

  const {
    data,
    isLoading,
    isUninitialized
  } = useGetUserQuery(
    shouldSkip
      ? skipToken
      : userId
  )

  if (isUninitialized) {
    return <p>Нет данных для загрузки</p>
  }

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

  return (
    <div>
      <h2>{data.name}</h2>
      <p>{data.email}</p>
    </div>
  )
}