В RTK Query имена endpoints, тегов, reducer path, хуков
и связанных сущностей формируют основу архитектуры слоя работы с
серверными данными. Непродуманное именование быстро приводит к
проблемам:
Грамотная система именования позволяет:
Название endpoint должно:
Плохие примеры:
user
data
load
request
item
Хорошие примеры:
getUsers
getUserById
createUser
updateUser
deleteUser
searchUsers
Наиболее распространённый стиль:
getПример:
endpoints: (builder) => ({
getUsers: builder.query({
query: () => '/users',
}),
getUserById: builder.query({
query: (id) => `/users/${id}`,
}),
createUser: builder.mutation({
query: (body) => ({
url: '/users',
method: 'POST',
body,
}),
}),
updateUser: builder.mutation({
query: ({ id, ...body }) => ({
url: `/users/${id}`,
method: 'PUT',
body,
}),
}),
deleteUser: builder.mutation({
query: (id) => ({
url: `/users/${id}`,
method: 'DELETE',
}),
}),
})
Такой подход даёт важные преимущества:
RTK Query генерирует хуки автоматически:
getUsers
↓
useGetUsersQuery
createUser
↓
useCreateUserMutation
Если endpoint называется неудачно:
users
то generated hook становится:
useUsersQuery
Название теряет смысл:
Поэтому endpoint должен быть максимально явным.
Стандарт:
getUsers
getPosts
getProducts
Пример:
getProducts: builder.query({
query: () => '/products',
})
Стандарт:
getUserById
getPostById
getProductById
Пример:
getUserById: builder.query({
query: (id) => `/users/${id}`,
})
getArticleBySlug
Пример:
getArticleBySlug: builder.query({
query: (slug) => `/articles/${slug}`,
})
Используется префикс:
searchUsers
searchProducts
searchArticles
Пример:
searchProducts: builder.query({
query: (term) => ({
url: '/products/search',
params: { q: term },
}),
})
Если endpoint выполняет фильтрацию:
filterProducts
filterOrders
Но чаще предпочтительнее сохранить универсальный endpoint:
getProducts
и передавать фильтры параметрами:
getProducts: builder.query({
query: (params) => ({
url: '/products',
params,
}),
})
Это уменьшает количество endpoint-ов.
Не рекомендуется:
getProductsPage
Лучше:
getProducts
с параметрами:
useGetProductsQuery({
page: 1,
limit: 20,
})
Пагинация — это состояние запроса, а не отдельный тип endpoint.
Стандарт:
createUser
createOrder
createComment
Стандарт:
updateUser
updateProfile
updateSettings
Если проект разделяет PUT и PATCH:
patchUser
patchSettings
или:
partialUpdateUser
Первый вариант короче и чаще используется.
Стандарт:
deleteUser
deleteComment
deleteProduct
Не рекомендуется:
removeUser
destroyUser
eraseUser
Причина — отсутствие единообразия.
Стандартные варианты:
login
logout
refreshToken
register
Пример:
login: builder.mutation({
query: (credentials) => ({
url: '/auth/login',
method: 'POST',
body: credentials,
}),
})
uploadAvatar
uploadDocument
uploadImage
exportUsers
exportReport
importProducts
importUsers
bulkDeleteUsers
bulkUpdateProducts
bulkCreateTags
Префикс bulk сразу показывает пакетную операцию.
Плохо:
getData
save
update
Хорошо:
getUserSettings
saveDraftArticle
updateShippingAddress
Чем больше проект — тем важнее конкретика.
userApi
authApi
productApi
orderApi
Пример:
export const userApi = createApi({
reducerPath: 'userApi',
endpoints: () => ({}),
})
Обычно совпадает с именем API slice:
reducerPath: 'userApi'
Не рекомендуется:
api
data
store
Причина — возможные конфликты.
Теги используются для:
Пример:
providesTags: ['User']
invalidatesTags: ['User']
Тег должен:
Наиболее распространённый стиль:
'User'
'Post'
'Comment'
'Product'
Преимущества:
Рекомендуется использовать единственное число:
'User'
а не:
'Users'
Причина — тег описывает тип сущности, а не коллекцию.
tagTypes: ['User']
getUsers: builder.query({
query: () => '/users',
providesTags: ['User'],
})
createUser: builder.mutation({
query: (body) => ({
url: '/users',
method: 'POST',
body,
}),
invalidatesTags: ['User'],
})
Для точечной инвалидации:
providesTags: (result, error, id) => [
{ type: 'User', id },
]
Тег:
{ type: 'User', id: 15 }
означает конкретного пользователя.
Очень распространённый паттерн:
{ type: 'User', id: 'LIST' }
Пример:
getUsers: builder.query({
query: () => '/users',
providesTags: (result) =>
result
? [
...result.map(({ id }) => ({
type: 'User',
id,
})),
{ type: 'User', id: 'LIST' },
]
: [{ type: 'User', id: 'LIST' }],
})
Это позволяет:
Наиболее распространённые значения:
'LIST'
'PARTIAL-LIST'
Иногда:
'DETAIL'
'STATS'
Важно соблюдать единый стиль.
Рекомендуется:
'LIST'
а не:
'list'
'users'
'all'
Причины:
tagTypes: [
'User',
'Post',
'Comment',
'Category',
]
tagTypes: [
'Data',
'List',
'Item',
]
Слишком абстрактные теги делают invalidate непредсказуемым.
Пример:
'UserPost'
'OrderItem'
'ProductReview'
Если API имеет отдельные endpoints статистики:
'UserStats'
'SalesStats'
'DashboardStats'
Обычно фильтры не выносятся в отдельные теги.
Плохо:
'ActiveUsers'
'ArchivedPosts'
Лучше:
'User'
'Post'
RTK Query сам разделяет кэш по аргументам query.
providesTags: ['Data']
invalidatesTags: ['Data']
Последствия:
tagTypes: ['User']
providesTags: (result, error, id) => [
{ type: 'User', id },
]
Такой подход делает invalidate точечным.
Главное правило — единая система.
Если выбран стиль:
getUserById
createUser
updateUser
deleteUser
то нельзя смешивать:
fetchUser
removeUser
saveUser
Плохо:
get
list
item
Плохо:
getAllUsersListFromServer
Хорошо:
getUsers
Плохо:
createUser
removeUser
patchUser
saveUser
Лучше:
createUser
updateUser
deleteUser
Плохо:
getEntity
getRecord
Хорошо:
getInvoice
getCustomer
getShipment
Крупные проекты часто используют доменное именование.
Пример:
billingApi
inventoryApi
analyticsApi
crmApi
Endpoints:
getInvoices
createInvoice
payInvoice
cancelInvoice
Такой подход особенно полезен в enterprise-системах.
Часто используется префикс домена:
user/getUsers
auth/login
catalog/getProducts
Но внутри RTK Query чаще достаточно domain API slice:
userApi
catalogApi
Хорошо структурированный API создаёт предсказуемые хуки:
useGetUsersQuery
useGetUserByIdQuery
useCreateUserMutation
useDeleteUserMutation
Это значительно улучшает DX.
getUsers
getUserById
searchUsers
createUser
updateUser
deleteUser
userApi
authApi
productApi
'User'
'Product'
'Order'
{ type: 'User', id: 'LIST' }
export const userApi = createApi({
reducerPath: 'userApi',
baseQuery: fetchBaseQuery({
baseUrl: '/api',
}),
tagTypes: ['User'],
endpoints: (builder) => ({
getUsers: builder.query({
query: () => '/users',
providesTags: (result) =>
result
? [
...result.map(({ id }) => ({
type: 'User',
id,
})),
{ type: 'User', id: 'LIST' },
]
: [{ type: 'User', id: 'LIST' }],
}),
getUserById: builder.query({
query: (id) => `/users/${id}`,
providesTags: (result, error, id) => [
{ type: 'User', id },
],
}),
createUser: builder.mutation({
query: (body) => ({
url: '/users',
method: 'POST',
body,
}),
invalidatesTags: [
{ type: 'User', id: 'LIST' },
],
}),
updateUser: builder.mutation({
query: ({ id, ...body }) => ({
url: `/users/${id}`,
method: 'PUT',
body,
}),
invalidatesTags: (result, error, { id }) => [
{ type: 'User', id },
],
}),
deleteUser: builder.mutation({
query: (id) => ({
url: `/users/${id}`,
method: 'DELETE',
}),
invalidatesTags: [
{ type: 'User', id: 'LIST' },
],
}),
}),
})