В основе RTK Query лежит кэш, организованный не как классическая «нормализованная база», а как набор изолированных записей, привязанных к запросам. Каждый запрос формирует собственный ключ кэша, зависящий от:
Таким образом, один и тот же endpoint с разными аргументами создает разные сегменты данных в кэше. Это принципиально отличает RTK Query от Redux-структур, где данные часто нормализуются вручную через entity adapter.
Хотя RTK Query не требует явной нормализации, его внутренняя архитектура уже решает часть задач, которые обычно закрывает нормализация:
Каждый запрос существует в кэше как отдельная сущность:
getUsers({ page: 1 })
getUsers({ page: 2 })
Эти данные не объединяются автоматически, но и не конфликтуют.
При работе с вложенными структурами возникает типичная проблема:
{
"id": 1,
"name": "Alice",
"posts": [
{ "id": 10, "title": "A" },
{ "id": 11, "title": "B" }
]
}
Если один и тот же пост встречается в разных списках пользователей или разных страницах, RTK Query:
Это приводит к необходимости дополнительной стратегии нормализации на уровне приложения.
Одним из основных инструментов является
transformResponse. Он позволяет преобразовать данные сразу
после получения ответа:
getUsers: builder.query({
query: () => "/users",
transformResponse: (response) => {
return response.map(user => ({
id: user.id,
fullName: `${user.firstName} ${user.lastName}`,
}));
}
});
На этом уровне можно:
Однако важно, что это преобразование локально для endpoint и не влияет на другие кэши.
Полная нормализация достигается через использование
createEntityAdapter из Redux Toolkit.
import { createEntityAdapter } from "@reduxjs/toolkit";
const usersAdapter = createEntityAdapter();
const initialState = usersAdapter.getInitialState();
Далее данные приводятся к форме:
transformResponse: (response) => {
return usersAdapter.setAll(initialState, response);
}
Структура становится:
{
ids: [1, 2, 3],
entities: {
1: { id: 1, name: "Alice" },
2: { id: 2, name: "Bob" }
}
}
Такой подход позволяет:
Даже без entity adapter можно имитировать нормализацию на уровне выбора данных:
const selectUsers = api.endpoints.getUsers.select();
const selectUserById = (id) => (result) =>
result.data?.find(user => user.id === id);
С использованием selectFromResult:
useGetUsersQuery(undefined, {
selectFromResult: ({ data }) => ({
user: data?.find(u => u.id === 5)
})
});
Этот механизм позволяет:
При пагинации или infinite scroll возникает задача объединения
данных. RTK Query не делает это автоматически, но предоставляет
merge:
getPosts: builder.query({
query: (page) => `/posts?page=${page}`,
serializeQueryArgs: ({ endpointName }) => endpointName,
merge: (currentCache, newItems) => {
currentCache.push(...newItems);
},
forceRefetch: ({ currentArg, previousArg }) => {
return currentArg !== previousArg;
}
});
Здесь достигается псевдонормализованная структура:
Для поддержания согласованности данных используется
updateQueryData:
api.util.updateQueryData("getUsers", undefined, (draft) => {
const user = draft.find(u => u.id === 1);
if (user) {
user.name = "Updated";
}
});
Это позволяет:
Система тегов в RTK Query играет ключевую роль в управлении связями данных:
providesTags: (result) =>
result
? [
...result.map(({ id }) => ({ type: "User", id })),
{ type: "User", id: "LIST" }
]
: [{ type: "User", id: "LIST" }];
И при мутациях:
invalidatesTags: [{ type: "User", id: "LIST" }]
Это создает логическую сеть зависимостей, которая частично заменяет необходимость глубокой нормализации:
Ключ к пониманию «как именно нормализуются данные» лежит в сериализации аргументов:
serializeQueryArgs: ({ endpointName, queryArgs }) => {
return endpointName;
};
Такой подход:
Если же оставить стандартную сериализацию, кэш будет фрагментирован.
В реальных приложениях часто используется гибридный подход:
Так формируется многоуровневая система, в которой нормализация распределена между несколькими механизмами, а не сосредоточена в одном месте.
Правильное распределение данных влияет на:
Нормализованные структуры позволяют работать с минимальными изменениями состояния, тогда как денормализованные кэши RTK Query проще, но могут приводить к лишним обновлениям UI при больших массивах данных.
Архитектура RTK Query сознательно избегает жесткой нормализации по умолчанию. Причины:
Это переносит ответственность за нормализацию на уровень конкретного приложения, где можно выбрать баланс между простотой и строгой структурой данных.
При работе с глубоко вложенными объектами часто применяются комбинированные техники:
Пример преобразования:
transformResponse: (response) => {
return {
users: usersAdapter.setAll(initialState, response.users),
posts: postsAdapter.setAll(initialState, response.posts),
};
}
Такой подход позволяет перейти от денормализованного API к структурированной модели внутри клиента.
В типичной архитектуре RTK Query данные проходят несколько стадий:
Эта цепочка формирует распределенную систему нормализации, где каждая часть решает свою задачу, а не дублирует общую ответственность.