Связанные сущности

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

Основой работы с связанными сущностями является нормализация данных на уровне клиентского кэша. Хотя RTK Query не требует явной нормализации, логика построения API часто опирается на стабильные идентификаторы.

Типичная сущность должна иметь:

  • уникальный идентификатор (id)
  • предсказуемую структуру
  • возможность быть переиспользованной в разных запросах

Пример структуры:

{
  id: 42,
  title: "Post title",
  authorId: 7
}

Даже без явного entity adapter подхода, RTK Query использует query cache key + endpoint + аргументы как основу хранения, что позволяет моделировать связи через повторное использование данных и тегирование.

Механизм тегов как основа связей

Связанные сущности в RTK Query реализуются через систему тегов:

  • providesTags
  • invalidatesTags

Эта система превращает endpoint в декларацию зависимостей между сущностями.

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

getPosts: builder.query({
  query: () => '/posts',
  providesTags: (result) =>
    result
      ? [
          ...result.map(({ id }) => ({ type: 'Post', id })),
          { type: 'Post', id: 'LIST' }
        ]
      : [{ type: 'Post', id: 'LIST' }]
})

Каждый элемент списка становится отдельной сущностью в системе тегов. Это позволяет обновлять как весь список, так и отдельный объект.

Инвалидация и каскадные обновления

Связанные сущности начинают проявлять себя при изменениях данных. При мутации:

addPost: builder.mutation({
  query: (body) => ({
    url: '/posts',
    method: 'POST',
    body
  }),
  invalidatesTags: [{ type: 'Post', id: 'LIST' }]
})

обновление списка автоматически приводит к перезапросу всех зависимых query, которые предоставляют этот тег.

Это формирует каскад:

  1. мутация изменяет серверное состояние
  2. invalidatesTags помечает сущности устаревшими
  3. RTK Query инициирует повторные запросы
  4. кэш синхронизируется

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

Связи “один ко многим”

Частый сценарий — связь пользователя и его сущностей:

  • user → posts
  • post → comments
getUserPosts: builder.query({
  query: (userId) => `/users/${userId}/posts`,
  providesTags: (result, error, userId) =>
    result
      ? [
          { type: 'UserPosts', id: userId },
          ...result.map(post => ({ type: 'Post', id: post.id }))
        ]
      : [{ type: 'UserPosts', id: userId }]
})

Здесь одновременно существуют два уровня сущностей:

  • агрегированная сущность (UserPosts)
  • индивидуальные сущности (Post)

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

Связанные сущности через аргументы запросов

Аргументы endpoint становятся частью ключа кэша, что позволяет моделировать отношения без явной нормализации.

getCommentsByPost: builder.query({
  query: (postId) => `/posts/${postId}/comments`,
  providesTags: (result, error, postId) =>
    result
      ? [
          { type: 'Comments', id: postId }
        ]
      : []
})

Каждый postId формирует независимую сущность в кэше. Это создаёт изолированные графы данных внутри одного API slice.

Граф зависимостей между сущностями

При усложнении API формируется граф:

  • Post
  • Comment
  • User
  • Reaction

Связи выражаются не напрямую, а через пересечение тегов.

Пример:

  • Post предоставляет {Post, id}
  • Comments предоставляют {Comments, postId}
  • User предоставляет {User, id}

При изменении пользователя:

invalidatesTags: [{ type: 'User', id }]

могут автоматически обновляться:

  • список постов пользователя
  • комментарии, если они включают userId
  • реакции

Это достигается не явными связями, а перекрытием тегов.

Использование selectFromResult для локальных связей

Связанные сущности могут формироваться на уровне селектора:

const { data: post } = useGetPostQuery(id, {
  selectFromResult: ({ data }) => ({
    data,
    authorId: data?.authorId
  })
})

Это позволяет извлекать часть сущности и использовать её для запуска зависимых запросов:

const { data: author } = useGetUserQuery(authorId, {
  skip: !authorId
})

Так формируется цепочка зависимых сущностей:

  • Post → User
  • User → Posts
  • Post → Comments

Композиция сущностей в UI-слое

Связанные сущности в RTK Query проявляются особенно явно при композиции компонентов:

  • список сущностей
  • детальная сущность
  • вложенные сущности

Каждый уровень может использовать собственный cache key, но при этом ссылаться на один и тот же источник данных.

Пример:

  • список постов использует getPosts
  • карточка поста использует getPost
  • комментарии используют getCommentsByPost

Несмотря на разные endpoints, теговая система синхронизирует их состояние.

Конфликты и консистентность данных

При наличии связанных сущностей возникает проблема устаревших связей. RTK Query решает её через:

  • единый источник истины на уровне cache
  • tag-based invalidation
  • автоматическое refetching

Если комментарий изменился:

invalidatesTags: (result, error, arg) => [
  { type: 'Comments', id: arg.postId },
  { type: 'Post', id: arg.postId }
]

обновляются сразу две сущности:

  • комментарии
  • пост (например, счетчик комментариев)

Частичная нормализация через теги

Хотя RTK Query не является полноценным ORM, теги позволяют имитировать поведение реляционной модели:

  • первичные ключи → id
  • внешние ключи → postId, userId
  • связи → providesTags / invalidatesTags

Это создаёт гибридную модель между REST-кэшированием и нормализованным хранилищем.

Дедупликация связанных запросов

При повторном обращении к одной сущности RTK Query предотвращает лишние запросы:

  • одинаковые аргументы → один cache entry
  • одинаковые теги → единая точка обновления

Это особенно важно для связанных сущностей, где один userId может использоваться в десятках компонентов одновременно.

Итеративное расширение связей

Связи между сущностями обычно развиваются постепенно:

  1. базовые endpoints без связей
  2. добавление providesTags
  3. введение invalidatesTags
  4. появление каскадных обновлений
  5. формирование графа сущностей

На этом этапе API начинает вести себя как реактивная система данных, а не набор запросов.

Стабильность связей при масштабировании

При росте приложения ключевыми становятся:

  • строгая схема тегов
  • консистентные идентификаторы
  • отсутствие дублирующих типов тегов
  • централизованная политика invalidation

Нарушение этих правил приводит к разрыву связей между сущностями и неконтролируемым устареванием кэша.