Query endpoints в RTK Query предназначены для получения
данных с сервера. Каждый query endpoint описывает:
Query endpoints являются центральной частью API-среза
(api slice) и определяют способ взаимодействия клиентского
приложения с сервером.
Базовый endpoint создаётся внутри createApi через секцию
endpoints.
import { createApi, fetchBaseQuery } fr om '@reduxjs/toolkit/query/react'
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: 'https://jsonplaceholder.typicode.com'
}),
endpoints: (builder) => ({
getPosts: builder.query({
query: () => '/posts'
})
})
})
В данном примере:
getPosts — имя query endpoint;builder.query() — создание query endpoint;query() — функция генерации HTTP-запроса.Метод builder.query() принимает объект конфигурации.
builder.query({
query,
transformResponse,
transformErrorResponse,
providesTags,
keepUnusedDataFor,
extraOptions,
async onQueryStarted(),
async onCacheEntryAdded()
})
Основные свойства:
| Свойство | Назначение |
|---|---|
query |
Описание HTTP-запроса |
transformResponse |
Изменение успешного ответа |
transformErrorResponse |
Изменение ошибки |
providesTags |
Связь с системой тегов |
keepUnusedDataFor |
Время хранения кеша |
onQueryStarted |
Побочные эффекты |
onCacheEntryAdded |
Работа с жизненным циклом кеша |
getUsers: builder.query({
query: () => '/users'
})
RTK Query автоматически:
После создания endpoint RTK Query автоматически генерирует React Hook.
export const {
useGetUsersQuery
} = api
Использование:
function Users() {
const { data, error, isLoading } = useGetUsersQuery()
if (isLoading) {
return <div>Loading...</div>
}
if (error) {
return <div>Error</div>
}
return (
<ul>
{data.map(user => (
<li key={user.id}>
{user.name}
</li>
))}
</ul>
)
}
Query endpoint может принимать аргументы.
getPost: builder.query({
query: (id) => `/posts/${id}`
})
Использование:
const { data } = useGetPostQuery(5)
RTK Query использует аргумент как часть ключа кеша.
Например:
useGetPostQuery(1)
useGetPostQuery(2)
создают два независимых кеша.
Функция query() может возвращать не только строку, но и
объект конфигурации.
getPost: builder.query({
query: (id) => ({
url: `/posts/${id}`,
method: 'GET'
})
})
getPosts: builder.query({
query: (page = 1) => ({
url: '/posts',
params: {
page
}
})
})
Результирующий запрос:
/posts?page=1
getProfile: builder.query({
query: () => ({
url: '/profile',
headers: {
Authorization: 'Bearer token'
}
})
})
Чаще всего заголовки задаются через prepareHeaders.
const baseQuery = fetchBaseQuery({
baseUrl: '/api',
prepareHeaders: (headers, { getState }) => {
const token = getState().auth.token
if (token) {
headers.set('Authorization', `Bearer ${token}`)
}
return headers
}
})
Аргумент endpoint может быть любого типа:
getPosts: builder.query({
query: ({ page, lim it }) => ({
url: '/posts',
params: {
page,
limit
}
})
})
Использование:
useGetPostsQuery({
page: 1,
limit: 20
})
RTK Query сериализует аргументы запроса для генерации cache key.
useGetPostsQuery({
page: 1
})
и
useGetPostsQuery({
page: 1
})
используют одинаковый cache key.
Разные аргументы создают разные записи кеша.
useGetPostsQuery({ page: 1 })
useGetPostsQuery({ page: 2 })
Query endpoint проходит несколько стадий:
RTK Query предоставляет множество флагов состояния.
const {
data,
error,
isLoading,
isFetching,
isSuccess,
isError
} = useGetUsersQuery()
isLoading активен только при первом запросе.
if (isLoading) {
return <Spinner />
}
isFetching активен при любом повторном запросе.
if (isFetching) {
console.log('Background refetch')
}
if (isSuccess) {
console.log(data)
}
if (isError) {
console.log(error)
}
Если компонент вызывает:
useGetUsersQuery()
и данные уже находятся в кеше, RTK Query:
Каждый hook создаёт подписку на cache entry.
Если несколько компонентов используют одинаковый query:
useGetUsersQuery()
RTK Query хранит один кеш и несколько подписчиков.
После удаления последнего подписчика запускается таймер очистки кеша.
По умолчанию:
60 секунд
Настройка:
getUsers: builder.query({
query: () => '/users',
keepUnusedDataFor: 300
})
RTK Query умеет автоматически обновлять данные.
useGetUsersQuery(undefined, {
refetchOnMountOrArgChange: true
})
const { data } = useGetUsersQuery(undefined, {
refetchOnFocus: true
})
Повторный запрос произойдёт при возврате во вкладку браузера.
const { data } = useGetUsersQuery(undefined, {
refetchOnReconnect: true
})
RTK Query поддерживает периодическое обновление.
const { data } = useGetNotificationsQuery(undefined, {
pollingInterval: 5000
})
Запрос выполняется каждые 5 секунд.
Иногда запрос необходимо отключить.
const { data } = useGetUserQuery(id, {
skip: !id
})
Для TypeScript и сложных условий используется
skipToken.
import { skipToken } fr om '@reduxjs/toolkit/query'
const result = useGetUserQuery(id ?? skipToken)
Позволяет минимизировать лишние рендеры.
const { user } = useGetUsersQuery(undefined, {
selectFromResult: ({ data }) => ({
user: data?.find(user => user.id === 5)
})
})
Позволяет изменить серверный ответ.
getUsers: builder.query({
query: () => '/users',
transformResponse: (response) => {
return response.data
}
})
transformResponse: (response) => {
const entities = {}
response.forEach(user => {
entities[user.id] = user
})
return entities
}
getUsers: builder.query({
query: () => '/users',
transformErrorResponse: (response) => {
return response.status
}
})
Query endpoints могут предоставлять теги.
getPosts: builder.query({
query: () => '/posts',
providesTags: ['Posts']
})
getPosts: builder.query({
query: () => '/posts',
providesTags: (result) =>
result
? [
...result.map(post => ({
type: 'Posts',
id: post.id
})),
{ type: 'Posts', id: 'LIST' }
]
: [{ type: 'Posts', id: 'LIST' }]
})
Mutation endpoint может инвалидировать конкретный тег.
updatePost: builder.mutation({
query: (post) => ({
url: `/posts/${post.id}`,
method: 'PUT',
body: post
}),
invalidatesTags: (result, error, post) => [
{ type: 'Posts', id: post.id }
]
})
Такой подход позволяет:
Вместо query можно использовать
queryFn.
getUser: builder.query({
async queryFn(id) {
try {
const response = await fetch(`/users/${id}`)
const data = await response.json()
return { data }
} catch (error) {
return { error }
}
}
})
| query | queryFn |
|---|---|
| Простые HTTP-запросы | Полный контроль |
| Использует baseQuery | Может не использовать baseQuery |
| Минимум кода | Гибкая логика |
Позволяет выполнять побочные эффекты.
getUsers: builder.query({
query: () => '/users',
async onQueryStarted(arg, api) {
console.log('Request started')
try {
await api.queryFulfilled
console.log('Success')
} catch {
console.log('Error')
}
}
})
async onQueryStarted(arg, {
dispatch,
getState,
queryFulfilled,
requestId,
extra,
getCacheEntry
}) {
}
Хотя оптимистичные обновления чаще используются в mutation endpoints, query endpoints тоже могут работать с кешем.
async onQueryStarted(id, { dispatch, queryFulfilled }) {
const patchResult = dispatch(
api.util.updateQueryData(
'getPost',
id,
draft => {
draft.views++
}
)
)
try {
await queryFulfilled
} catch {
patchResult.undo()
}
}
Позволяет реагировать на создание кеша.
getMessages: builder.query({
query: () => '/messages',
async onCacheEntryAdded(
arg,
{
updateCachedData,
cacheDataLoaded,
cacheEntryRemoved
}
) {
const socket = new WebSocket('ws://localhost:3000')
try {
await cacheDataLoaded
socket.onmess age = (event) => {
const message = JSON.parse(event.data)
updateCachedData((draft) => {
draft.push(message)
})
}
} catch {}
await cacheEntryRemoved
socket.close()
}
})
Метод updateCachedData использует Immer.
updateCachedData((draft) => {
draft.push(newMessage)
})
Можно мутировать draft напрямую.
Промис выполняется после первой успешной загрузки данных.
await cacheDataLoaded
Позволяет ожидать удаление кеша.
await cacheEntryRemoved
RTK Query поддерживает lazy queries.
const [
trigger,
result
] = useLazyGetUsersQuery()
<button onCl ick={() => trigger()}>
Load users
</button>
RTK Query поддерживает предварительную загрузку данных.
const prefetchPosts = api.usePrefetch('getPosts')
<button
onMouseEn ter={() => prefetchPosts()}
>
Open posts
</button>
prefetchPosts(undefined, {
force: true
})
Позволяет получать только состояние query.
const result = api.endpoints.getUsers.useQueryState()
Подписка без чтения данных.
api.endpoints.getUsers.useQuerySubscription()
Стандартная ошибка RTK Query:
{
status: 404,
data: {
message: 'Not found'
}
}
if (error) {
console.log(error.status)
}
Для lazy queries доступен unwrap().
try {
const data = await trigger().unwrap()
} catch (error) {
console.log(error)
}
getPosts: builder.query({
query: ({ page, lim it }) => ({
url: '/posts',
method: 'GET',
params: {
page,
limit
}
}),
transformResponse: (response) => {
return response.items
},
providesTags: (result) =>
result
? [
...result.map(post => ({
type: 'Posts',
id: post.id
})),
{ type: 'Posts', id: 'LIST' }
]
: [{ type: 'Posts', id: 'LIST' }],
keepUnusedDataFor: 120,
async onQueryStarted(arg, {
queryFulfilled
}) {
try {
await queryFulfilled
} catch (error) {
console.log(error)
}
}
})
Крупные API лучше разбивать логически.
/users
/posts
/comments
/auth
Обычно приложение использует один createApi.
export const api = createApi({
reducerPath: 'api',
baseQuery,
tagTypes: ['Posts', 'Users'],
endpoints: () => ({})
})
Для code splitting используется injectEndpoints.
const extendedApi = api.injectEndpoints({
endpoints: (builder) => ({
getUsers: builder.query({
query: () => '/users'
})
})
})
RTK Query оптимизирует:
Проблема:
useGetPostsQuery({
page: currentPage
})
Если объект создаётся нестабильно, возможны лишние операции сериализации.
Без providesTags автоматическая инвалидация не
работает.
Нежелательно копировать query data в local state.
Плохо:
const [users, setUsers] = useState([])
useEffect(() => {
setUsers(data)
}, [data])
RTK Query уже предоставляет реактивное состояние.
| Query | Mutation |
|---|---|
| Получение данных | Изменение данных |
| Кешируются автоматически | Обычно вызывают инвалидацию |
| GET-запросы | POST/PUT/PATCH/DELETE |
| useQuery hooks | useMutation hooks |
Каждый query endpoint внутри RTK Query:
Пример внутреннего состояния:
{
api: {
queries: {
'getPosts(undefined)': {
status: 'fulfilled',
data: [...],
error: null
}
}
}
}