Трансформация данных с помощью transformResponse

В RTK Query каждый запрос проходит строго определённую цепочку обработки: формирование аргументов, выполнение baseQuery, получение ответа и его последующая нормализация перед попаданием в кэш. Именно на этапе постобработки ответа применяется transformResponse.

transformResponse позволяет изменить структуру данных, возвращаемых сервером, до того как они будут сохранены в Redux-кэше и переданы в компоненты через автоматически сгенерированные хуки.

Ключевая идея заключается в том, что слой API-ответа не обязан совпадать с тем, как данные должны выглядеть в приложении.


Базовая концепция transformResponse

transformResponse — это функция, которая получает:

  • результат baseQuery
  • метаданные запроса (в расширенных сценариях)

и возвращает преобразованное значение, которое становится итоговым data в RTK Query.

Общий вид:

transformResponse: (response, meta, arg) => {
  return response;
}

Первый аргумент — фактический ответ сервера. Второй и третий используются при необходимости учитывать HTTP-метаданные или параметры запроса.


Простой пример преобразования структуры

Часто сервер возвращает данные в обёртке:

{
  "data": {
    "items": [1, 2, 3],
    "total": 3
  },
  "status": "ok"
}

В приложении обычно требуется только items.

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

getUsers: builder.query({
  query: () => '/users',
  transformResponse: (response) => {
    return response.data.items;
  }
});

После этого в компоненте data будет сразу массив, без промежуточных обёрток.


Извлечение и нормализация данных

RTK Query не навязывает нормализацию данных, но transformResponse часто используется для приведения структуры к удобному виду.

Пример нормализации:

transformResponse: (response) => {
  return response.users.reduce((acc, user) => {
    acc[user.id] = user;
    return acc;
  }, {});
}

Результат — объект с доступом по идентификатору:

{
  "1": { id: 1, name: "Alex" },
  "2": { id: 2, name: "Anna" }
}

Это упрощает дальнейшую работу с кэшем и снижает необходимость дополнительной обработки в компонентах.


Использование transformResponse с метаданными

В некоторых случаях требуется учитывать HTTP-уровень ответа:

transformResponse: (response, meta) => {
  if (meta?.response?.status === 204) {
    return [];
  }

  return response.data;
}

Здесь meta содержит информацию о сетевом ответе, включая статус-код и заголовки (в зависимости от baseQuery).

Это позволяет централизованно обрабатывать нестандартные ответы API.


Зависимость transformResponse от аргументов запроса

Третий параметр arg позволяет учитывать параметры запроса при трансформации.

Пример:

getPosts: builder.query({
  query: (limit) => `/posts?limit=${limit}`,
  transformResponse: (response, meta, arg) => {
    if (arg > 10) {
      return response.data.slice(0, 10);
    }
    return response.data;
  }
});

Здесь логика трансформации зависит от входного параметра limit.


Разделение ответственности между baseQuery и transformResponse

Важно разграничивать обязанности:

  • baseQuery отвечает за получение данных (fetch, axios и т.д.)
  • transformResponse отвечает за структуру данных в приложении

Неправильный подход — пытаться в baseQuery выполнять бизнес-трансформации. Это усложняет повторное использование и тестирование.

Правильная архитектура:

  • baseQuery — транспортный уровень
  • transformResponse — слой адаптации API под приложение

Композиция transformResponse с цепочкой обработки

RTK Query позволяет комбинировать несколько уровней преобразования данных:

  1. baseQuery получает сырой ответ
  2. transformResponse преобразует структуру
  3. selectFromResult дополнительно фильтрует данные на уровне хука

Пример:

transformResponse: (response) => response.data.items

и далее в компоненте:

useGetUsersQuery(undefined, {
  selectFromResult: ({ data }) => ({
    activeUsers: data?.filter(u => u.active)
  })
});

Таким образом, transformResponse задаёт глобальный формат, а selectFromResult — локальную оптимизацию.


Обработка вложенных API-структур

Некоторые API возвращают глубоко вложенные структуры:

{
  "payload": {
    "result": {
      "users": [...]
    }
  }
}

transformResponse позволяет централизованно упростить доступ:

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

Это снижает связанность UI с форматом API.


Преобразование дат и типов данных

Частый кейс — приведение строковых значений к нужным типам:

transformResponse: (response) => {
  return response.map(item => ({
    ...item,
    createdAt: new Date(item.createdAt),
    price: Number(item.price)
  }));
}

Такой подход избавляет компоненты от необходимости выполнять преобразования при каждом рендере.


Фильтрация данных на уровне transformResponse

Фильтрация также является частью трансформации:

transformResponse: (response) => {
  return response.data.filter(item => item.isActive);
}

Это позволяет исключить невалидные или неиспользуемые данные до попадания в кэш RTK Query.


Устойчивость к изменениям API

Одно из ключевых преимуществ transformResponse — изоляция UI от изменений API.

Если сервер изменит структуру:

{
  "result": [...]
}

вместо:

{
  "data": [...]
}

достаточно обновить только одну функцию:

transformResponse: (response) => response.result;

Компоненты и хуки при этом остаются неизменными.


Ограничения и особенности поведения

Несмотря на гибкость, transformResponse имеет важные особенности:

  • результат кэшируется в преобразованном виде
  • повторный запрос не вызывает повторную трансформацию уже закэшированных данных
  • трансформация должна быть синхронной
  • побочные эффекты внутри функции недопустимы

Любая асинхронная логика должна быть вынесена за пределы RTK Query.


Влияние на кэширование

Преобразованные данные становятся частью кэша RTK Query. Это означает, что:

  • структура кэша зависит от transformResponse
  • изменение трансформации может привести к изменению формы данных в кэше
  • при рефакторинге API важно учитывать миграцию состояния

Пример:

transformResponse: (response) => response.items

и затем изменение на:

transformResponse: (response) => response.data.items

фактически меняет контракт кэша.


Использование с TypeScript (контекстно)

Хотя логика остаётся идентичной, transformResponse часто используется для приведения типов к более удобным формам:

  • удаление null-полей
  • нормализация API DTO в domain model
  • унификация разных источников данных

Даже в JavaScript-ориентированных проектах это позволяет поддерживать единообразие структуры данных.


Практика композиции трансформаций

В реальных проектах часто применяется многоуровневая обработка:

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

  return items
    .filter(item => item.isActive)
    .map(item => ({
      id: item.id,
      title: item.title.trim(),
      createdAt: new Date(item.createdAt)
    }));
}

Такой подход объединяет извлечение, фильтрацию и нормализацию в одном месте, что делает слой API предсказуемым и централизованным.


Типичные ошибки при использовании transformResponse

На практике часто встречаются следующие проблемы:

  • дублирование бизнес-логики в нескольких endpoints
  • попытка выполнять асинхронные операции
  • изменение глобальных переменных внутри трансформации
  • чрезмерная сложность функции, превращающая её в мини-сервис

Оптимальный подход — сохранять transformResponse как чистую функцию преобразования данных без побочных эффектов и внешних зависимостей.