createAsyncThunk требует много boilerplate: состояния loading/error, кэширование, refetch... RTK Query автоматизирует всё это.
RTK Query vs createAsyncThunk:
| Функция | createAsyncThunk | RTK Query |
|---------|------------------|-----------|
| Loading состояние | Вручную | Автоматически |
| Кэширование | Вручную | Автоматически |
| Дедупликация | Вручную | Автоматически |
| Refetch | Вручную | Автоматически |
| Оптимистичные обновления | Сложно | Встроено |
| Invalidation | Вручную | Теги |
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['Post', 'User'],
endpoints: (builder) => ({
// Query — GET запросы
getPosts: builder.query({
query: () => '/posts',
providesTags: ['Post']
}),
getPostById: builder.query({
query: (id) => `/posts/${id}`,
providesTags: (result, error, id) => [{ type: 'Post', id }]
}),
// Mutation — POST/PUT/DELETE
createPost: builder.mutation({
query: (newPost) => ({
url: '/posts',
method: 'POST',
body: newPost
}),
invalidatesTags: ['Post'] // Автоматически refetch getPosts
}),
deletePost: builder.mutation({
query: (id) => ({
url: `/posts/${id}`,
method: 'DELETE'
}),
invalidatesTags: (result, error, id) => [{ type: 'Post', id }]
})
})
})
// Автогенерированные хуки
export const {
useGetPostsQuery,
useGetPostByIdQuery,
useCreatePostMutation,
useDeletePostMutation
} = api
export default apiimport { configureStore } from '@reduxjs/toolkit'
import api from './api'
const store = configureStore({
reducer: {
[api.reducerPath]: api.reducer,
// другие редьюсеры
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware)
})function PostsList() {
const {
data: posts, // Данные
isLoading, // Первая загрузка
isFetching, // Любая загрузка (включая refetch)
isSuccess, // Успех
isError, // Ошибка
error, // Объект ошибки
refetch // Функция для повторного запроса
} = useGetPostsQuery()
if (isLoading) return <Spinner />
if (isError) return <Error message={error.message} />
return (
<div>
{isFetching && <RefetchIndicator />}
{posts.map(post => <PostCard key={post.id} post={post} />)}
<button onClick={refetch}>Обновить</button>
</div>
)
}function PostPage({ postId }) {
const { data: post, isLoading } = useGetPostByIdQuery(postId)
// Пропуск запроса если нет id
const { data } = useGetPostByIdQuery(postId, {
skip: !postId
})
// Polling каждые 30 секунд
const { data: liveData } = useGetPostsQuery(undefined, {
pollingInterval: 30000
})
}function CreatePostForm() {
const [createPost, { isLoading, isSuccess, isError }] = useCreatePostMutation()
const handleSubmit = async (formData) => {
try {
await createPost(formData).unwrap()
// Успех! getPosts автоматически обновится благодаря invalidatesTags
} catch (error) {
console.error('Ошибка:', error)
}
}
return (
<form onSubmit={handleSubmit}>
{isLoading && <span>Сохранение...</span>}
{isSuccess && <span>Сохранено!</span>}
{isError && <span>Ошибка!</span>}
<button type="submit" disabled={isLoading}>
Создать пост
</button>
</form>
)
}endpoints: (builder) => ({
getUsers: builder.query({
query: () => '/users',
providesTags: (result) =>
result
? [
...result.map(({ id }) => ({ type: 'User', id })),
{ type: 'User', id: 'LIST' }
]
: [{ type: 'User', id: 'LIST' }]
}),
updateUser: builder.mutation({
query: ({ id, ...patch }) => ({
url: `/users/${id}`,
method: 'PATCH',
body: patch
}),
// Инвалидирует только конкретного пользователя
invalidatesTags: (result, error, { id }) => [{ type: 'User', id }]
}),
deleteUser: builder.mutation({
query: (id) => ({
url: `/users/${id}`,
method: 'DELETE'
}),
// Инвалидирует весь список
invalidatesTags: [{ type: 'User', id: 'LIST' }]
})
})updatePost: builder.mutation({
query: ({ id, ...patch }) => ({
url: `/posts/${id}`,
method: 'PATCH',
body: patch
}),
async onQueryStarted({ id, ...patch }, { dispatch, queryFulfilled }) {
// Оптимистично обновляем кэш
const patchResult = dispatch(
api.util.updateQueryData('getPosts', undefined, (draft) => {
const post = draft.find(p => p.id === id)
if (post) Object.assign(post, patch)
})
)
try {
await queryFulfilled
} catch {
// Откатываем при ошибке
patchResult.undo()
}
}
})Симуляция RTK Query: автоматические хуки для data fetching
// Упрощённая симуляция RTK Query
function createApi({ baseUrl, endpoints }) {
const cache = new Map()
const subscribers = new Map()
const endpointDefs = endpoints({
query: (config) => ({ type: 'query', ...config }),
mutation: (config) => ({ type: 'mutation', ...config })
})
const hooks = {}
Object.entries(endpointDefs).forEach(([name, def]) => {
if (def.type === 'query') {
// Создаём useXxxQuery хук (симуляция)
hooks['use' + name.charAt(0).toUpperCase() + name.slice(1) + 'Query'] = (arg) => {
const cacheKey = name + ':' + JSON.stringify(arg)
return cache.get(cacheKey) || { isLoading: true, data: undefined }
}
}
})
return { hooks, endpointDefs, cache }
}
// === Создаём API ===
const postsApi = createApi({
baseUrl: '/api',
endpoints: (builder) => ({
getPosts: builder.query({
query: () => '/posts',
providesTags: ['Post']
}),
getPostById: builder.query({
query: (id) => '/posts/' + id,
providesTags: (result, error, id) => [{ type: 'Post', id }]
}),
createPost: builder.mutation({
query: (newPost) => ({
url: '/posts',
method: 'POST',
body: newPost
}),
invalidatesTags: ['Post']
})
})
})
console.log('=== RTK Query Concepts ===\n')
console.log('Созданные endpoints:')
Object.entries(postsApi.endpointDefs).forEach(([name, def]) => {
console.log(' ' + name + ':', def.type, def.query ? def.query(123) : def.mutation)
})
console.log('\nАвтогенерированные хуки:')
Object.keys(postsApi.hooks).forEach(hook => {
console.log(' ' + hook)
})
console.log('\n--- Как работает RTK Query ---')
console.log(`
1. Определяем endpoints (query/mutation)
2. RTK Query генерирует:
- useGetPostsQuery() — хук для получения всех постов
- useGetPostByIdQuery(id) — хук для получения поста по ID
- useCreatePostMutation() — хук для создания поста
3. Хуки автоматически:
- Кэшируют данные
- Отслеживают loading/error состояния
- Дедуплицируют одинаковые запросы
- Инвалидируют кэш через теги
`)
// Демонстрация использования
console.log('\n--- Пример использования в компоненте ---')
console.log(`
function PostsList() {
const { data, isLoading, error } = useGetPostsQuery()
if (isLoading) return <Loading />
if (error) return <Error />
return data.map(post => <Post key={post.id} {...post} />)
}
`)createAsyncThunk требует много boilerplate: состояния loading/error, кэширование, refetch... RTK Query автоматизирует всё это.
RTK Query vs createAsyncThunk:
| Функция | createAsyncThunk | RTK Query |
|---------|------------------|-----------|
| Loading состояние | Вручную | Автоматически |
| Кэширование | Вручную | Автоматически |
| Дедупликация | Вручную | Автоматически |
| Refetch | Вручную | Автоматически |
| Оптимистичные обновления | Сложно | Встроено |
| Invalidation | Вручную | Теги |
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['Post', 'User'],
endpoints: (builder) => ({
// Query — GET запросы
getPosts: builder.query({
query: () => '/posts',
providesTags: ['Post']
}),
getPostById: builder.query({
query: (id) => `/posts/${id}`,
providesTags: (result, error, id) => [{ type: 'Post', id }]
}),
// Mutation — POST/PUT/DELETE
createPost: builder.mutation({
query: (newPost) => ({
url: '/posts',
method: 'POST',
body: newPost
}),
invalidatesTags: ['Post'] // Автоматически refetch getPosts
}),
deletePost: builder.mutation({
query: (id) => ({
url: `/posts/${id}`,
method: 'DELETE'
}),
invalidatesTags: (result, error, id) => [{ type: 'Post', id }]
})
})
})
// Автогенерированные хуки
export const {
useGetPostsQuery,
useGetPostByIdQuery,
useCreatePostMutation,
useDeletePostMutation
} = api
export default apiimport { configureStore } from '@reduxjs/toolkit'
import api from './api'
const store = configureStore({
reducer: {
[api.reducerPath]: api.reducer,
// другие редьюсеры
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(api.middleware)
})function PostsList() {
const {
data: posts, // Данные
isLoading, // Первая загрузка
isFetching, // Любая загрузка (включая refetch)
isSuccess, // Успех
isError, // Ошибка
error, // Объект ошибки
refetch // Функция для повторного запроса
} = useGetPostsQuery()
if (isLoading) return <Spinner />
if (isError) return <Error message={error.message} />
return (
<div>
{isFetching && <RefetchIndicator />}
{posts.map(post => <PostCard key={post.id} post={post} />)}
<button onClick={refetch}>Обновить</button>
</div>
)
}function PostPage({ postId }) {
const { data: post, isLoading } = useGetPostByIdQuery(postId)
// Пропуск запроса если нет id
const { data } = useGetPostByIdQuery(postId, {
skip: !postId
})
// Polling каждые 30 секунд
const { data: liveData } = useGetPostsQuery(undefined, {
pollingInterval: 30000
})
}function CreatePostForm() {
const [createPost, { isLoading, isSuccess, isError }] = useCreatePostMutation()
const handleSubmit = async (formData) => {
try {
await createPost(formData).unwrap()
// Успех! getPosts автоматически обновится благодаря invalidatesTags
} catch (error) {
console.error('Ошибка:', error)
}
}
return (
<form onSubmit={handleSubmit}>
{isLoading && <span>Сохранение...</span>}
{isSuccess && <span>Сохранено!</span>}
{isError && <span>Ошибка!</span>}
<button type="submit" disabled={isLoading}>
Создать пост
</button>
</form>
)
}endpoints: (builder) => ({
getUsers: builder.query({
query: () => '/users',
providesTags: (result) =>
result
? [
...result.map(({ id }) => ({ type: 'User', id })),
{ type: 'User', id: 'LIST' }
]
: [{ type: 'User', id: 'LIST' }]
}),
updateUser: builder.mutation({
query: ({ id, ...patch }) => ({
url: `/users/${id}`,
method: 'PATCH',
body: patch
}),
// Инвалидирует только конкретного пользователя
invalidatesTags: (result, error, { id }) => [{ type: 'User', id }]
}),
deleteUser: builder.mutation({
query: (id) => ({
url: `/users/${id}`,
method: 'DELETE'
}),
// Инвалидирует весь список
invalidatesTags: [{ type: 'User', id: 'LIST' }]
})
})updatePost: builder.mutation({
query: ({ id, ...patch }) => ({
url: `/posts/${id}`,
method: 'PATCH',
body: patch
}),
async onQueryStarted({ id, ...patch }, { dispatch, queryFulfilled }) {
// Оптимистично обновляем кэш
const patchResult = dispatch(
api.util.updateQueryData('getPosts', undefined, (draft) => {
const post = draft.find(p => p.id === id)
if (post) Object.assign(post, patch)
})
)
try {
await queryFulfilled
} catch {
// Откатываем при ошибке
patchResult.undo()
}
}
})Симуляция RTK Query: автоматические хуки для data fetching
// Упрощённая симуляция RTK Query
function createApi({ baseUrl, endpoints }) {
const cache = new Map()
const subscribers = new Map()
const endpointDefs = endpoints({
query: (config) => ({ type: 'query', ...config }),
mutation: (config) => ({ type: 'mutation', ...config })
})
const hooks = {}
Object.entries(endpointDefs).forEach(([name, def]) => {
if (def.type === 'query') {
// Создаём useXxxQuery хук (симуляция)
hooks['use' + name.charAt(0).toUpperCase() + name.slice(1) + 'Query'] = (arg) => {
const cacheKey = name + ':' + JSON.stringify(arg)
return cache.get(cacheKey) || { isLoading: true, data: undefined }
}
}
})
return { hooks, endpointDefs, cache }
}
// === Создаём API ===
const postsApi = createApi({
baseUrl: '/api',
endpoints: (builder) => ({
getPosts: builder.query({
query: () => '/posts',
providesTags: ['Post']
}),
getPostById: builder.query({
query: (id) => '/posts/' + id,
providesTags: (result, error, id) => [{ type: 'Post', id }]
}),
createPost: builder.mutation({
query: (newPost) => ({
url: '/posts',
method: 'POST',
body: newPost
}),
invalidatesTags: ['Post']
})
})
})
console.log('=== RTK Query Concepts ===\n')
console.log('Созданные endpoints:')
Object.entries(postsApi.endpointDefs).forEach(([name, def]) => {
console.log(' ' + name + ':', def.type, def.query ? def.query(123) : def.mutation)
})
console.log('\nАвтогенерированные хуки:')
Object.keys(postsApi.hooks).forEach(hook => {
console.log(' ' + hook)
})
console.log('\n--- Как работает RTK Query ---')
console.log(`
1. Определяем endpoints (query/mutation)
2. RTK Query генерирует:
- useGetPostsQuery() — хук для получения всех постов
- useGetPostByIdQuery(id) — хук для получения поста по ID
- useCreatePostMutation() — хук для создания поста
3. Хуки автоматически:
- Кэшируют данные
- Отслеживают loading/error состояния
- Дедуплицируют одинаковые запросы
- Инвалидируют кэш через теги
`)
// Демонстрация использования
console.log('\n--- Пример использования в компоненте ---')
console.log(`
function PostsList() {
const { data, isLoading, error } = useGetPostsQuery()
if (isLoading) return <Loading />
if (error) return <Error />
return data.map(post => <Post key={post.id} {...post} />)
}
`)Создай приложение с RTK Query паттерном: список пользователей с загрузкой, добавлением и удалением. Реализуй кэширование и автоматическое обновление списка после мутаций.
Для инвалидации кэша используй setVersion(v => v + 1). Это заставит useQuery перезапросить данные.