По умолчанию RTK Query автоматически выполняет запрос сразу после
вызова query-хука. Если компонент использует
useGetUsersQuery(), запрос будет отправлен немедленно после
рендера.
Во многих сценариях такое поведение неудобно:
Для решения подобных задач RTK Query предоставляет механизм условного выполнения запросов — conditional fetching.
Стандартный query-хук выглядит следующим образом:
const { data, isLoading } = useGetPostsQuery()
После рендера компонента RTK Query:
Если query принимает аргументы:
const { data } = useGetUserQuery(userId)
RTK Query автоматически отслеживает изменение
userId.
При изменении аргумента:
Предположим, идентификатор пользователя ещё неизвестен:
const userId = undefined
const { data } = useGetUserQuery(userId)
В этом случае запрос может отправиться с некорректным URL:
/users/undefined
Либо сервер вернёт ошибку:
400 Bad Request
Ещё один распространённый сценарий — поиск:
const [search, setSearch] = useState('')
const { data } = useSearchQuery(search)
Если пользователь ничего не ввёл, приложение начнёт выполнять бессмысленные запросы.
Основной механизм conditional fetching — параметр
skip.
const { data } = useGetUserQuery(userId, {
skip: true
})
При skip: true:
uninitialized.Наиболее частый вариант:
const { data, isLoading } = useGetUserQuery(userId, {
skip: !userId
})
Логика:
userId существует — запрос выполняется;userId отсутствует — запрос пропускается.Когда запрос пропущен через 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>
}
const [email, setEmail] = useState('')
const { data } = useFindUserQuery(email)
Проблемы:
const [email, setEmail] = useState('')
const { data } = useFindUserQuery(email, {
skip: email.length < 3
})
Теперь запрос выполнится только после ввода минимум трёх символов.
RTK Query предоставляет специальный объект
skipToken.
Импорт:
import { skipToken } from '@reduxjs/toolkit/query'
Использование:
const { data } = useGetUserQuery(
userId ?? skipToken
)
useGetUserQuery(userId, {
skip: !userId
})
useGetUserQuery(userId ?? skipToken)
skipToken считается более чистым и безопасным
вариантом.
Особенно в TypeScript.
Проблема:
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
})
Если токен отсутствует:
skip может содержать любую логику.
const shouldSkip =
!token ||
!userId ||
isModalClosed
const { data } = useGetUserQuery(userId, {
skip: shouldSkip
})
Иногда запрос не должен выполняться автоматически вообще.
Например:
Для этого используется lazy query.
RTK Query генерирует специальные lazy hooks.
const { data } = useGetUsersQuery()
const [trigger, result] = useLazyGetUsersQuery()
Запрос не выполняется автоматически.
Вызов происходит вручную:
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>
)
}
const [getUser, result] =
useLazyGetUserQuery()
const handleSelect = id => {
getUser(id)
}
trigger возвращает Promise.
const [getUser] = useLazyGetUserQuery()
const handleLoad = async () => {
const result = await getUser(15)
console.log(result)
}
Как и mutation, lazy query поддерживает unwrap.
const [getUser] = useLazyGetUserQuery()
try {
const user = await getUser(5).unwrap()
console.log(user)
} catch (error) {
console.log(error)
}
unwrap():
throw;const { data } = useGetUserQuery(userId, {
skip: !isModalOpen
})
Запрос выполняется только после открытия модального окна.
const { data: users } =
useGetUsersQuery(undefined, {
skip: activeTab !== 'users'
})
Иногда данные уже есть в store.
Можно избегать повторной загрузки:
const cachedUser = useSelector(
state => state.users.entities[id]
)
const { data } = useGetUserQuery(id, {
skip: !!cachedUser
})
Частый сценарий — цепочка запросов.
Например:
const { data: user } =
useGetUserQuery(userId)
const { data: posts } =
useGetPostsByUserQuery(user?.id, {
skip: !user
})
Второй запрос начнётся только после успешной загрузки пользователя.
Polling можно отключать динамически.
const { data } = useGetNotificationsQuery(undefined, {
pollingInterval: isFocused ? 5000 : 0
})
Либо полностью пропускать запрос:
const { data } = useGetNotificationsQuery(undefined, {
skip: !isFocused
})
pollingInterval: 0
skip: true
Важно понимать:
const { data } = useGetUsersQuery(undefined, {
skip: true
})
RTK Query не удаляет существующий кеш.
Он просто:
Если позже skip станет false, RTK Query
повторно подключится к кешу.
const { data } = useGetUsersQuery(undefined, {
skip: disabled
})
Когда disabled меняется:
RTK Query:
RTK Query:
Если запрос пропущен:
const { refetch } = useGetUsersQuery(undefined, {
skip: true
})
refetch() работать не будет, потому что query не
инициализирован.
Для ручного запуска следует использовать lazy query:
const [fetchUsers] =
useLazyGetUsersQuery()
При серверном рендеринге conditional fetching особенно важен.
Например:
const { data } = useGetProfileQuery(undefined, {
skip: typeof window === 'undefined'
})
Это предотвращает выполнение browser-only запросов на сервере.
Ошибка:
const [enabled, setEnabled] = useState(false)
const { refetch } = useGetUsersQuery(undefined, {
skip: !enabled
})
const handleClick = () => {
refetch()
}
Проблема:
refetch() не работает.Правильнее:
const [fetchUsers] =
useLazyGetUsersQuery()
fetchUsers()
Ошибка:
skip: userId === undefined
Если userId = null, запрос всё равно выполнится.
Безопаснее:
skip: !userId
Либо:
userId ?? skipToken
Ошибка:
useSearchQuery(search)
Исправление:
useSearchQuery(search, {
skip: search.length < 2
})
const result = useGetUsersQuery(undefined, {
skip: !enabled,
selectFromResult: ({ data }) => ({
users: data ?? []
})
})
Это позволяет:
Наиболее распространённые сценарии:
| Сценарий | Решение |
|---|---|
| Нет ID | skip |
| Нет токена | skip |
| Загрузка по кнопке | lazy query |
| Поиск | skip |
| Зависимые запросы | skip |
| Tabs | skip |
| Модальные окна | skip |
| SSR | skip |
| TypeScript-safe skip | skipToken |
skip: !requiredData
Подходит для:
useLazyQuery()
Подходит для:
id ?? skipToken
Наиболее безопасный вариант типизации.
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>
)
}