Преобразование запросов

RTK Query предоставляет несколько механизмов преобразования входящих и исходящих данных. Они позволяют:

  • изменять структуру ответа сервера;
  • адаптировать данные под формат интерфейса;
  • преобразовывать параметры запроса;
  • централизованно обрабатывать ошибки;
  • нормализовать данные;
  • объединять данные из разных API;
  • выполнять предобработку до попадания данных в Redux Store.

Основные инструменты преобразования:

  • transformResponse
  • transformErrorResponse
  • кастомные queryFn
  • преобразование аргументов запросов
  • адаптеры сущностей
  • селекторы и мемоизация

transformResponse

transformResponse используется для изменения данных после получения ответа сервера, но до сохранения в 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'
        }
    }
}

Использование createEntityAdapter

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: {...}
    }
}

transformResponse с meta

Некоторые 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
                })
            )
        }));
    }
})

transformErrorResponse

Позволяет централизованно преобразовывать ошибки.

Базовый пример:

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
        }
    };
}

Подготовка данных для UI

Иногда 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: [...]
}

Преобразование query аргументов

Преобразование может происходить ещё до выполнения запроса.

getUsers: builder.query({
    query: (params) => {

        return {
            url: '/users',

            params: {
                page: params.page || 1,
                lim it: params.limit || 20,
                search: params.search?.trim()
            }
        };
    }
})

Очистка query параметров

query: (params) => {

    const filtered =
        Object.fromEntries(
            Object.entries(params).filter(
                ([_, value]) =>
                    value !== undefined &&
                    value !== null &&
                    value !== ''
            )
        );

    return {
        url: '/products',
        params: filtered
    };
}

Преобразование mutation body

updateUser: builder.mutation({
    query: (user) => ({

        url: `/users/${user.id}`,

        method: 'PUT',

        body: {
            first_name: user.firstName,
            last_name: user.lastName
        }
    })
})

Использование queryFn

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}`;
}

Работа с GraphQL

RTK Query можно адаптировать под GraphQL API.

getUsers: builder.query({
    query: () => ({

        url: '/graphql',

        method: 'POST',

        body: {
            query: `
                query {
                    users {
                        id
                        name
                    }
                }
            `
        }
    }),

    transformResponse: (response) => {
        return response.data.users;
    }
})

Комбинирование transformResponse и providesTags

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)
    }));
}

Подобные операции лучше:

  • мемоизировать;
  • переносить в selectors;
  • вычислять на уровне UI;
  • кэшировать отдельно.

Повторное использование преобразований

Полезно выносить трансформации в отдельные функции.

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 должен быть чистой функцией.


Неправильная обработка null

transformResponse: (response) => {
    return response.data.items;
}

Проблема:

Cannot read property 'items'

Безопасный вариант:

transformResponse: (response) => {
    return response?.data?.items || [];
}

Слишком сложные преобразования

Громоздкая логика ухудшает поддержку:

transformResponse: (response) => {

    // 300 строк логики
}

Лучше:

transformResponse: mapComplexData

Архитектурные подходы

Thin API Layer

RTK Query хранит данные максимально близко к backend.

Преобразования минимальны.

Плюсы:

  • меньше логики;
  • проще debugging;
  • легче обновлять API.

Минусы:

  • UI содержит больше преобразований.

Rich API Layer

Все данные подготавливаются внутри RTK Query.

Плюсы:

  • UI становится проще;
  • единый формат данных;
  • меньше дублирования.

Минусы:

  • сложнее поддержка API слоя;
  • тяжёлые transformResponse.

Когда использовать transformResponse

Подходит для:

  • нормализации;
  • переименования полей;
  • фильтрации;
  • сортировки;
  • адаптации backend структуры;
  • подготовки cache;
  • вычисления derived fields.

Когда transformResponse использовать нежелательно

Нежелательно помещать туда:

  • побочные эффекты;
  • HTTP-логику;
  • сложную бизнес-логику;
  • тяжёлые вычисления;
  • обращения к DOM;
  • работу с localStorage;
  • асинхронные операции.

Практический пример полноценного преобразования

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'
                }
            ]
})