Динамические теги в RTK Query позволяют формировать зависимости между кэшированными данными не статически, а на основе результата запроса, параметров или состояния выполнения endpoint. Такой подход делает систему инвалидации гибкой и масштабируемой.
Статические теги подходят только для простых случаев:
providesTags: ['Post']
Однако в реальных приложениях обычно требуется:
Именно для этого используются динамические теги.
RTK Query позволяет передавать в providesTags и
invalidatesTags не только массив строк, но и функцию.
Сигнатура функции:
(result, error, arg) => tags
Аргументы:
| Аргумент | Описание |
|---|---|
result |
успешный результат запроса |
error |
объект ошибки |
arg |
аргумент endpoint |
Наиболее распространённый сценарий — создание тегов для каждой сущности из списка.
Пример API:
[
{ "id": 1, "title": "Post 1" },
{ "id": 2, "title": "Post 2" },
{ "id": 3, "title": "Post 3" }
]
Endpoint:
getPosts: builder.query({
query: () => '/posts',
providesTags: (result) =>
result
? [
...result.map(post => ({
type: 'Post',
id: post.id
})),
{ type: 'Post', id: 'LIST' }
]
: [{ type: 'Post', id: 'LIST' }]
})
После успешного запроса RTK Query создаёт набор тегов:
[
{ type: 'Post', id: 1 },
{ type: 'Post', id: 2 },
{ type: 'Post', id: 3 },
{ type: 'Post', id: 'LIST' }
]
Теперь кэш знает:
Это позволяет:
Конструкция:
{ type: 'Post', id: 'LIST' }
не является встроенной особенностью RTK Query. Это соглашение, которое используется разработчиками для обозначения списка.
RTK Query воспринимает 'LIST' как обычный
идентификатор.
Можно использовать любое значение:
{ type: 'Post', id: 'ALL' }
или:
{ type: 'Post', id: 'COLLECTION' }
Но 'LIST' считается общепринятым вариантом.
Mutation может инвалидировать только изменённый объект.
Пример:
updatePost: builder.mutation({
query: ({ id, ...patch }) => ({
url: `/posts/${id}`,
method: 'PATCH',
body: patch
}),
invalidatesTags: (result, error, arg) => [
{ type: 'Post', id: arg.id }
]
})
Предположим, ранее был выполнен запрос:
useGetPostsQuery()
Он зарегистрировал теги:
[
{ type: 'Post', id: 1 },
{ type: 'Post', id: 2 },
{ type: 'Post', id: 3 }
]
После выполнения mutation:
updatePost({ id: 2, title: 'Updated' })
RTK Query:
{ type: 'Post', id: 2 }
помечает их устаревшими;
автоматически запускает refetch.
При этом остальные сущности не затрагиваются.
При создании нового элемента обычно инвалидируется весь список.
addPost: builder.mutation({
query: (body) => ({
url: '/posts',
method: 'POST',
body
}),
invalidatesTags: [
{ type: 'Post', id: 'LIST' }
]
})
После создания новой записи:
Инвалидация только одного ID здесь не поможет, потому что новый объект ещё отсутствует в кэше списка.
Иногда mutation изменяет одновременно:
Пример:
invalidatesTags: (result, error, arg) => [
{ type: 'Post', id: arg.id },
{ type: 'Post', id: 'LIST' }
]
Такой подход применяется:
RTK Query передаёт аргумент endpoint третьим параметром.
Пример:
getPost: builder.query({
query: (id) => `/posts/${id}`,
providesTags: (result, error, id) => [
{ type: 'Post', id }
]
})
Даже если сервер вернул неполный объект:
{
"title": "Post"
}
endpoint всё равно знает:
id === 15
Следовательно, можно безопасно формировать тег.
Без динамических тегов страницы конфликтуют между собой.
Например:
useGetPostsQuery(1)
useGetPostsQuery(2)
Если обе страницы используют:
providesTags: ['Post']
то инвалидируется весь кэш сразу.
Пример:
getPosts: builder.query({
query: (page) => `/posts?page=${page}`,
providesTags: (result, error, page) => [
{ type: 'PostPage', id: page }
]
})
Теперь каждая страница имеет собственный тег:
{ type: 'PostPage', id: 1 }
{ type: 'PostPage', id: 2 }
invalidatesTags: (result, error, arg) => [
{ type: 'PostPage', id: arg.page }
]
Это уменьшает количество лишних refetch.
Динамические теги особенно полезны при сложной фильтрации.
Пример:
getPosts: builder.query({
query: ({ status }) => `/posts?status=${status}`,
providesTags: (result, error, arg) => [
{ type: 'PostFilter', id: arg.status }
]
})
Теперь:
useGetPostsQuery({ status: 'draft' })
и:
useGetPostsQuery({ status: 'published' })
получают разные теги.
Иногда одного параметра недостаточно.
Можно использовать составные ключи:
providesTags: (result, error, arg) => [
{
type: 'PostFilter',
id: `${arg.status}-${arg.page}`
}
]
или:
id: JSON.stringify(arg)
Пример:
providesTags: (result, error, filters) => [
{
type: 'Posts',
id: JSON.stringify(filters)
}
]
Для:
{
status: 'active',
sort: 'date',
page: 2
}
получится:
{
type: 'Posts',
id: '{"status":"active","sort":"date","page":2}'
}
У такого подхода есть проблемы:
{
a: 1,
b: 2
}
и:
{
b: 2,
a: 1
}
могут давать разные строки.
Сложные объекты создают длинные идентификаторы.
Нельзя отдельно инвалидировать:
status;page;sort.Лучше явно формировать ключ:
id: [
arg.status,
arg.sort,
arg.page
].join(':')
RTK Query не требует нормализации данных, но динамические теги позволяют приблизиться к entity-based архитектуре.
Пример:
providesTags: (result) =>
result
? result.ids.map(id => ({
type: 'Post',
id
}))
: []
Особенно полезно при использовании:
createEntityAdapter;Удаление — один из наиболее важных сценариев для динамических тегов.
Пример:
deletePost: builder.mutation({
query: (id) => ({
url: `/posts/${id}`,
method: 'DELETE'
}),
invalidatesTags: (result, error, id) => [
{ type: 'Post', id },
{ type: 'Post', id: 'LIST' }
]
})
Удаление влияет сразу на два состояния:
{ type: 'Post', id }
Нужно удалить или обновить детальную страницу.
{ type: 'Post', id: 'LIST' }
Нужно убрать элемент из списка.
При optimistic update теги используются для синхронизации после подтверждения сервера.
Пример:
updatePost: builder.mutation({
query: ({ id, ...patch }) => ({
url: `/posts/${id}`,
method: 'PATCH',
body: patch
}),
async onQueryStarted(arg, { dispatch, queryFulfilled }) {
const patchResult = dispatch(
api.util.updateQueryData(
'getPost',
arg.id,
draft => {
Object.assign(draft, arg)
}
)
)
try {
await queryFulfilled
} catch {
patchResult.undo()
}
},
invalidatesTags: (result, error, arg) => [
{ type: 'Post', id: arg.id }
]
})
Optimistic update обновляет локальный кэш, но:
Инвалидация гарантирует окончательную синхронизацию.
Функция может возвращать разные теги в зависимости от результата.
Пример:
providesTags: (result, error, arg) => {
if (error) {
return ['Error']
}
return [
{ type: 'Post', id: arg }
]
}
Если endpoint не должен участвовать в инвалидации:
providesTags: () => []
или:
invalidatesTags: () => []
При ошибке result может быть undefined.
Безопасный вариант:
providesTags: (result) =>
result
? result.map(post => ({
type: 'Post',
id: post.id
}))
: []
Неправильно:
providesTags: (result) => [
result.map(post => ({
type: 'Post',
id: post.id
}))
]
Результат:
[
[
{ type: 'Post', id: 1 },
{ type: 'Post', id: 2 }
]
]
RTK Query ожидает плоский массив.
Правильно:
[
...result.map(...)
]
В крупных проектах генерацию тегов выносят отдельно.
Пример:
const providesList = (type) => (result) =>
result
? [
...result.map(({ id }) => ({ type, id })),
{ type, id: 'LIST' }
]
: [{ type, id: 'LIST' }]
Использование:
providesTags: providesList('Post')
const providesById = (type) => (result, error, id) => [
{ type, id }
]
const invalidatesById = (type) => (result, error, id) => [
{ type, id }
]
При динамических тегах особенно важно правильно проектировать
tagTypes.
Пример:
tagTypes: [
'Post',
'User',
'Comment',
'Category'
]
Плохо:
tagTypes: ['Data']
Это приводит к:
Рекомендуется:
Пример:
'Post'
'PostList'
'PostPage'
'PostFilter'
Правильно организованные теги:
Они практически обязательны при:
RTK Query использует теги как систему зависимостей между запросами и mutation.
Статические теги описывают зависимость грубо.
Динамические теги позволяют: