RTK Query является частью экосистемы Redux Toolkit и изначально проектировался как решение для работы с серверным состоянием внутри Redux-приложений. В отличие от сторонних библиотек, требующих отдельной конфигурации и интеграционного слоя, RTK Query предоставляет единый архитектурный подход:
Такой подход уменьшает количество инфраструктурного кода и устраняет необходимость комбинировать несколько библиотек одновременно.
Пример типичной конфигурации:
import { configureStore } from '@reduxjs/toolkit'
import { api } from './services/api'
export const store = configureStore({
reducer: {
[api.reducerPath]: api.reducer
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware)
})
В большинстве альтернативных решений приходится отдельно реализовывать:
RTK Query включает всё это в единую систему.
Одним из главных преимуществ RTK Query является сокращение шаблонного кода.
Классический Redux-подход требует:
RTK Query автоматически генерирует значительную часть инфраструктуры.
export const fetchUsers = () => async (dispatch) => {
dispatch({ type: 'users/loading' })
try {
const response = await fetch('/api/users')
const data = await response.json()
dispatch({
type: 'users/success',
payload: data
})
} catch (error) {
dispatch({
type: 'users/error',
payload: error.message
})
}
}
const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: '/api'
}),
endpoints: (builder) => ({
getUsers: builder.query({
query: () => '/users'
})
})
})
export const {
useGetUsersQuery
} = api
Разница особенно заметна в крупных приложениях, содержащих десятки API-методов.
RTK Query предоставляет встроенную систему кеширования без дополнительной настройки.
После первого запроса данные сохраняются в store и переиспользуются:
const { data } = useGetUsersQuery()
При повторном использовании того же query:
Большинство альтернатив требуют ручной реализации кеширующего слоя либо сложной конфигурации.
Одним из наиболее сильных механизмов RTK Query считается tag-based invalidation.
Пример:
const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: '/api'
}),
tagTypes: ['Users'],
endpoints: (builder) => ({
getUsers: builder.query({
query: () => '/users',
providesTags: ['Users']
}),
createUser: builder.mutation({
query: (body) => ({
url: '/users',
method: 'POST',
body
}),
invalidatesTags: ['Users']
})
})
})
После выполнения createUser:
Во многих других решениях разработчику приходится самостоятельно:
RTK Query генерирует хуки автоматически:
export const {
useGetUsersQuery,
useCreateUserMutation
} = api
Преимущества:
Пример использования:
function Users() {
const {
data,
isLoading,
error
} = 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>
)
}
RTK Query позволяет хранить все endpoints в едином месте:
export const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({
baseUrl: '/api'
}),
endpoints: (builder) => ({
getUsers: builder.query({
query: () => '/users'
}),
getPosts: builder.query({
query: () => '/posts'
}),
getComments: builder.query({
query: () => '/comments'
})
})
})
Это создаёт:
В проектах с большим количеством REST endpoints это особенно важно.
RTK Query автоматически предотвращает дублирующиеся запросы.
Если несколько компонентов одновременно используют:
useGetUsersQuery()
то выполняется:
Это снижает:
RTK Query самостоятельно отслеживает:
Пример автоматического обновления данных:
const { data } = useGetUsersQuery(undefined, {
pollingInterval: 5000
})
Пример обновления при возврате на вкладку:
const { data } = useGetUsersQuery(undefined, {
refetchOnFocus: true
})
Подобные возможности в обычном Redux требуют значительного объёма дополнительной логики.
RTK Query поддерживает optimistic updates на уровне API.
Пример:
updateUser: builder.mutation({
query: ({ id, ...patch }) => ({
url: `/users/${id}`,
method: 'PATCH',
body: patch
}),
async onQueryStarted(arg, { dispatch, queryFulfilled }) {
const patchResult = dispatch(
api.util.updateQueryData(
'getUsers',
undefined,
(draft) => {
const user = draft.find(
user => user.id === arg.id
)
Object.assign(user, arg)
}
)
)
try {
await queryFulfilled
} catch {
patchResult.undo()
}
}
})
Преимущества:
RTK Query поддерживает:
Пример кастомного baseQuery:
const customBaseQuery = async (
args,
api,
extraOptions
) => {
const result = await fetchBaseQuery({
baseUrl: '/api'
})(args, api, extraOptions)
if (result.error?.status === 401) {
// refresh token logic
}
return result
}
Это позволяет адаптировать RTK Query под сложные enterprise-приложения.
RTK Query навязывает единый подход к работе с серверным состоянием.
Во многих командах это устраняет:
Типичная структура:
src/
├── app/
├── services/
│ └── api.js
├── features/
├── components/
└── pages/
Единый паттерн особенно полезен:
RTK Query предоставляет мощный inference типов.
Пример:
getUsers: builder.query<User[], void>({
query: () => '/users'
})
Типы автоматически распространяются:
Многие альтернативные решения требуют более сложной типизации.
RTK Query автоматически предоставляет:
isLoading;isFetching;isSuccess;isError;error;data;currentData.Пример:
const {
data,
isLoading,
isFetching,
isError
} = useGetUsersQuery()
Это устраняет необходимость вручную хранить статус запросов в Redux store.
RTK Query поддерживает отложенные запросы:
const [
trigger,
result
] = useLazyGetUsersQuery()
Пример:
<button onCl ick={() => trigger()}>
Load users
</button>
Преимущества:
Endpoints можно расширять и инжектировать динамически:
const extendedApi = api.injectEndpoints({
endpoints: (builder) => ({
getPosts: builder.query({
query: () => '/posts'
})
})
})
Это важно для:
RTK Query поддерживает server-side rendering.
Особенно полезно для:
Пример prefetch:
store.dispatch(
api.endpoints.getUsers.initiate()
)
Это облегчает интеграцию с серверным рендерингом без дополнительной инфраструктуры.
Поскольку RTK Query использует Redux store, все запросы видны через Redux DevTools.
Можно отслеживать:
Это значительно упрощает:
RTK Query автоматически очищает неиспользуемый кеш.
Пример:
keepUnusedDataFor: 60
После отсутствия подписчиков данные удаляются.
Преимущества:
RTK Query уменьшает число типичных ошибок:
Причина заключается в декларативной модели работы.
Вместо императивного управления состоянием:
dispatch(fetchUsers())
используется декларативный запрос:
useGetUsersQuery()
RTK Query самостоятельно управляет жизненным циклом данных.
На ранних этапах проекта RTK Query остаётся простым:
getUsers: builder.query({
query: () => '/users'
})
Но по мере роста приложения библиотека позволяет:
Это делает RTK Query универсальным решением как для небольших SPA, так и для крупных enterprise-систем.