RTK Query предоставляет несколько механизмов преобразования входящих и исходящих данных. Они позволяют:
Основные инструменты преобразования:
transformResponsetransformErrorResponsequeryFntransformResponse используется для изменения данных
после получения ответа сервера, но до сохранения в cache RTK Query.
Базовый синтаксис:
getUsers: builder.query({
query: () => '/users',
transformResponse: (response) => {
return response;
}
})
Параметры функции:
transformResponse: (
response,
meta,
arg
) => {}
Где:
response — данные сервера;meta — служебная информация;arg — аргумент запроса.Часто API возвращает лишнюю вложенность:
{
"status": "success",
"data": [
{
"id": 1,
"name": "Alex"
}
]
}
В приложении обычно требуется только data.
getUsers: builder.query({
query: () => '/users',
transformResponse: (response) => {
return response.data;
}
})
После преобразования хук вернёт:
[
{
id: 1,
name: 'Alex'
}
]
Backend и frontend часто используют разные naming conventions.
Сервер:
{
"user_id": 15,
"first_name": "John",
"last_name": "Smith"
}
Преобразование:
getUser: builder.query({
query: (id) => `/users/${id}`,
transformResponse: (response) => {
return {
id: response.user_id,
firstName: response.first_name,
lastName: response.last_name
};
}
})
Полученный объект:
{
id: 15,
firstName: 'John',
lastName: 'Smith'
}
Иногда необходимо создать дополнительные свойства.
getUsers: builder.query({
query: () => '/users',
transformResponse: (response) => {
return response.map(user => ({
...user,
fullName: `${user.firstName} ${user.lastName}`,
isAdult: user.age >= 18
}));
}
})
Преобразование удобно использовать для удаления ненужных элементов.
getProducts: builder.query({
query: () => '/products',
transformResponse: (response) => {
return response.filter(product => product.active);
}
})
RTK Query cache может сразу хранить подготовленные данные.
getPosts: builder.query({
query: () => '/posts',
transformResponse: (response) => {
return [...response].sort(
(a, b) => b.createdAt - a.createdAt
);
}
})
Крупные приложения часто используют нормализованную структуру.
Пример без нормализации:
[
{
id: 1,
name: 'Alex'
},
{
id: 2,
name: 'John'
}
]
После преобразования:
{
ids: [1, 2],
entities: {
1: {
id: 1,
name: 'Alex'
},
2: {
id: 2,
name: 'John'
}
}
}
RTK предоставляет встроенный адаптер сущностей.
import {
createEntityAdapter
} fr om '@reduxjs/toolkit';
const usersAdapter = createEntityAdapter();
const initialState =
usersAdapter.getInitialState();
Преобразование:
getUsers: builder.query({
query: () => '/users',
transformResponse: (response) => {
return usersAdapter.setAll(
initialState,
response
);
}
})
Результат:
{
ids: [1, 2, 3],
entities: {
1: {...},
2: {...},
3: {...}
}
}
Некоторые API передают важные заголовки.
Например:
X-Total-Count: 150
RTK Query позволяет получить headers через meta.
getUsers: builder.query({
query: () => '/users',
transformResponse: (
response,
meta
) => {
const total =
meta.response.headers.get(
'X-Total-Count'
);
return {
items: response,
total: Number(total)
};
}
})
Третий параметр — аргумент endpoint.
getUsers: builder.query({
query: (role) => ({
url: '/users',
params: { role }
}),
transformResponse: (
response,
meta,
role
) => {
return response.map(user => ({
...user,
requestedRole: role
}));
}
})
API часто возвращают строки.
{
"createdAt": "2026-05-20T10:15:00Z"
}
Преобразование:
getPosts: builder.query({
query: () => '/posts',
transformResponse: (response) => {
return response.map(post => ({
...post,
createdAt: new Date(post.createdAt)
}));
}
})
Теперь в приложении доступны реальные объекты Date.
getOrders: builder.query({
query: () => '/orders',
transformResponse: (response) => {
return response.map(order => ({
...order,
products: order.products.map(
product => ({
...product,
total:
product.price *
product.quantity
})
)
}));
}
})
Позволяет централизованно преобразовывать ошибки.
Базовый пример:
getUser: builder.query({
query: (id) => `/users/${id}`,
transformErrorResponse: (
response
) => {
return {
status: response.status,
message:
response.data?.message ||
'Unknown error'
};
}
})
Backend разных сервисов может возвращать разные структуры.
API №1:
{
"message": "Access denied"
}
API №2:
{
"error": {
"text": "Access denied"
}
}
Единый формат:
transformErrorResponse: (
response
) => {
return {
status: response.status,
message:
response.data?.message ||
response.data?.error?.text ||
'Unknown error'
};
}
Иногда требуется сохранить только часть ответа.
Сервер:
{
"token": "abc123",
"user": {
"id": 1,
"name": "Alex"
},
"permissions": [...]
}
Преобразование:
login: builder.mutation({
query: (credentials) => ({
url: '/login',
method: 'POST',
body: credentials
}),
transformResponse: (response) => {
return response.user;
}
})
Преобразование может агрегировать несколько частей ответа.
transformResponse: (response) => {
return {
users: response.users,
posts: response.posts,
stats: {
totalUsers:
response.users.length,
totalPosts:
response.posts.length
}
};
}
Иногда backend возвращает неудобный формат.
Исходный ответ:
[
{
"id": 1,
"category": "books",
"title": "Book 1"
},
{
"id": 2,
"category": "games",
"title": "Game 1"
}
]
Преобразование в grouped structure:
transformResponse: (response) => {
return response.reduce(
(acc, item) => {
if (!acc[item.category]) {
acc[item.category] = [];
}
acc[item.category].push(item);
return acc;
},
{}
);
}
Результат:
{
books: [...],
games: [...]
}
Преобразование может происходить ещё до выполнения запроса.
getUsers: builder.query({
query: (params) => {
return {
url: '/users',
params: {
page: params.page || 1,
lim it: params.limit || 20,
search: params.search?.trim()
}
};
}
})
query: (params) => {
const filtered =
Object.fromEntries(
Object.entries(params).filter(
([_, value]) =>
value !== undefined &&
value !== null &&
value !== ''
)
);
return {
url: '/products',
params: filtered
};
}
updateUser: builder.mutation({
query: (user) => ({
url: `/users/${user.id}`,
method: 'PUT',
body: {
first_name: user.firstName,
last_name: user.lastName
}
})
})
queryFn полностью заменяет стандартный
query.
Это наиболее гибкий механизм преобразования.
getUser: builder.query({
async queryFn(id, api, extraOptions, baseQuery) {
const result =
await baseQuery(`/users/${id}`);
if (result.error) {
return {
error: result.error
};
}
return {
data: {
...result.data,
fullName:
`${result.data.firstName} ` +
`${result.data.lastName}`
}
};
}
})
queryFn позволяет объединять несколько запросов.
getUserWithPosts: builder.query({
async queryFn(id, api, extra, baseQuery) {
const userResult =
await baseQuery(`/users/${id}`);
if (userResult.error) {
return {
error: userResult.error
};
}
const postsResult =
await baseQuery(
`/users/${id}/posts`
);
if (postsResult.error) {
return {
error: postsResult.error
};
}
return {
data: {
user: userResult.data,
posts: postsResult.data
}
};
}
})
getDashboard: builder.query({
async queryFn(_, api, extra, baseQuery) {
const [
users,
posts,
comments
] = await Promise.all([
baseQuery('/users'),
baseQuery('/posts'),
baseQuery('/comments')
]);
return {
data: {
users: users.data,
posts: posts.data,
comments: comments.data
}
};
}
})
Иногда API требует нестандартный формат query string.
query: (filters) => {
const params =
new URLSearchParams();
filters.tags.forEach(tag => {
params.append('tags[]', tag);
});
return `/posts?${params}`;
}
RTK Query можно адаптировать под GraphQL API.
getUsers: builder.query({
query: () => ({
url: '/graphql',
method: 'POST',
body: {
query: `
query {
users {
id
name
}
}
`
}
}),
transformResponse: (response) => {
return response.data.users;
}
})
getUsers: builder.query({
query: () => '/users',
transformResponse: (response) => {
return response.data;
},
providesTags: (result) =>
result
? [
...result.map(user => ({
type: 'Users',
id: user.id
})),
{
type: 'Users',
id: 'LIST'
}
]
: [
{
type: 'Users',
id: 'LIST'
}
]
})
Ошибка:
transformResponse: (response) => {
response.sort();
return response;
}
Проблема — изменение исходного массива.
Безопасный вариант:
transformResponse: (response) => {
return [...response].sort();
}
Тяжёлые вычисления внутри transformResponse могут
ухудшать производительность.
Проблемный пример:
transformResponse: (response) => {
return response.map(item => ({
...item,
hugeCalculation:
extremelyHeavyFunction(item)
}));
}
Подобные операции лучше:
Полезно выносить трансформации в отдельные функции.
const mapUser = (user) => ({
id: user.user_id,
firstName: user.first_name,
lastName: user.last_name
});
Использование:
transformResponse: (response) => {
return response.map(mapUser);
}
const normalizeUser = (user) => ({
...user,
fullName:
`${user.firstName} ${user.lastName}`
});
const filterActive = (users) =>
users.filter(user => user.active);
transformResponse: (response) => {
return filterActive(
response.map(normalizeUser)
);
}
Ошибка:
transformResponse: (response) => {
localStorage.setItem(
'data',
JSON.stringify(response)
);
return response;
}
transformResponse должен быть чистой функцией.
transformResponse: (response) => {
return response.data.items;
}
Проблема:
Cannot read property 'items'
Безопасный вариант:
transformResponse: (response) => {
return response?.data?.items || [];
}
Громоздкая логика ухудшает поддержку:
transformResponse: (response) => {
// 300 строк логики
}
Лучше:
transformResponse: mapComplexData
RTK Query хранит данные максимально близко к backend.
Преобразования минимальны.
Плюсы:
Минусы:
Все данные подготавливаются внутри RTK Query.
Плюсы:
Минусы:
Подходит для:
Нежелательно помещать туда:
const usersAdapter =
createEntityAdapter({
sortComparer:
(a, b) =>
a.name.localeCompare(b.name)
});
const initialState =
usersAdapter.getInitialState();
getUsers: builder.query({
query: () => '/users',
transformResponse: (
response
) => {
const prepared =
response.data
.filter(user => user.active)
.map(user => ({
id: user.user_id,
name:
`${user.first_name} ` +
`${user.last_name}`,
email: user.email,
createdAt:
new Date(user.created_at)
}));
return usersAdapter.setAll(
initialState,
prepared
);
},
providesTags: (result) =>
result
? [
...result.ids.map(id => ({
type: 'Users',
id
})),
{
type: 'Users',
id: 'LIST'
}
]
: [
{
type: 'Users',
id: 'LIST'
}
]
})