Теги и их использование

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

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

Ключевая идея заключается в разделении ответственности:

  • запросы описывают, какие данные они предоставляют;
  • мутации описывают, какие данные становятся устаревшими.

providesTags: описание принадлежности данных

Поле providesTags используется внутри query-эндпоинтов. Оно определяет, какие теги ассоциируются с результатом запроса.

Каждый запрос может:

  • предоставлять один тег;
  • предоставлять несколько тегов;
  • динамически формировать список тегов на основе полученных данных.

Базовая форма:

getPosts: builder.query({
  query: () => '/posts',
  providesTags: ['Posts']
})

В этом случае весь результат запроса связан с тегом Posts. Любая мутация, инвалидирующая этот тег, приведёт к повторному запросу.


Динамическое тегирование на основе данных

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

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

Здесь реализуется два уровня тегирования:

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

Это позволяет:

  • обновлять только один элемент;
  • обновлять весь список при необходимости.

invalidatesTags: управление устареванием данных

Поле invalidatesTags используется в мутациях. Оно указывает, какие теги должны считаться устаревшими после выполнения операции.

Простейший пример:

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

После успешного добавления нового поста все запросы, связанные с тегом Post, будут автоматически перезапрошены.


Связь providesTags и invalidatesTags

Механизм работает через сопоставление типов и идентификаторов:

  • providesTags помечает данные как актуальные;
  • invalidatesTags помечает их как устаревшие;
  • совпадение приводит к рефетчу.

Пример связки:

getPost: builder.query({
  query: (id) => `/posts/${id}`,
  providesTags: (result, error, id) => [{ type: 'Post', id }]
})

updatePost: builder.mutation({
  query: ({ id, ...patch }) => ({
    url: `/posts/${id}`,
    method: 'PATCH',
    body: patch
  }),
  invalidatesTags: (result, error, { id }) => [{ type: 'Post', id }]
})

В этом случае обновление конкретного поста приводит к рефетчу только этого поста, а не всего списка.


Использование LIST-тегов для коллекций

Одной из распространённых практик является использование специального идентификатора LIST. Он представляет весь набор данных.

providesTags: [
  { type: 'Post', id: 'LIST' }
]

Мутации могут инвалидировать весь список:

invalidatesTags: [
  { type: 'Post', id: 'LIST' }
]

Это удобно в случаях:

  • добавления новых элементов;
  • удаления элементов;
  • массовых изменений;
  • необходимости полного обновления списка.

Частичная инвалидация данных

RTK Query поддерживает точечное обновление. Это особенно важно для производительности.

Пример:

updateComment: builder.mutation({
  query: ({ postId, commentId, ...patch }) => ({
    url: `/posts/${postId}/comments/${commentId}`,
    method: 'PATCH',
    body: patch
  }),
  invalidatesTags: (result, error, { postId, commentId }) => [
    { type: 'Comment', id: commentId }
  ]
})

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


Комбинирование нескольких типов тегов

В реальных приложениях данные часто связаны между собой. Один запрос может предоставлять несколько типов тегов.

getPostWithComments: builder.query({
  query: (id) => `/posts/${id}?include=comments`,
  providesTags: (result, error, id) => [
    { type: 'Post', id },
    { type: 'Comment', id: 'LIST' }
  ]
})

Такая модель позволяет:

  • обновлять пост отдельно от комментариев;
  • инвалидировать комментарии без затрагивания поста;
  • строить сложные зависимости.

Условное тегирование

Иногда данные могут отсутствовать или быть ошибочными. В таких случаях providesTags должен учитывать состояние результата.

providesTags: (result, error, id) =>
  error
    ? []
    : result
      ? [{ type: 'Post', id }]
      : []

Это предотвращает некорректную привязку кэша к несуществующим данным.


Поведение при ошибках

Важно учитывать, что:

  • при ошибке запроса providesTags может не сработать;
  • invalidation всё равно может инициировать рефетч;
  • RTK Query не кэширует ошибочные результаты как валидные данные.

Пример поведения:

getUser: builder.query({
  query: (id) => `/users/${id}`,
  providesTags: (result, error, id) =>
    error ? [] : [{ type: 'User', id }]
})

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


Расширенные сценарии использования

Синхронизация связанных сущностей

При обновлении одной сущности может потребоваться обновление другой:

updateOrder: builder.mutation({
  query: (order) => ({
    url: `/orders/${order.id}`,
    method: 'PUT',
    body: order
  }),
  invalidatesTags: (result, error, order) => [
    { type: 'Order', id: order.id },
    { type: 'User', id: order.userId }
  ]
})

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


Глобальная инвалидация

Иногда требуется сбросить весь кэш определённого типа:

logout: builder.mutation({
  queryFn: () => ({ data: true }),
  invalidatesTags: ['Post', 'User', 'Comment']
})

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


Типизация тегов в масштабируемых проектах

В крупных приложениях важно избегать случайных ошибок в строковых тегах. Обычно применяются константы:

const TAGS = {
  POST: 'Post',
  COMMENT: 'Comment',
  USER: 'User'
}

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

providesTags: [{ type: TAGS.POST, id: 'LIST' }]

Это снижает риск рассинхронизации между query и mutation слоями.


Производительность и гранулярность тегов

Чем более точечные теги используются, тем:

  • меньше лишних запросов;
  • быстрее обновление UI;
  • ниже нагрузка на сеть.

Однако чрезмерная детализация может привести к:

  • усложнению логики;
  • росту количества тегов;
  • трудностям в отладке.

Баланс достигается через комбинацию:

  • LIST-тегов для массовых операций;
  • entity-тегов для точечных обновлений.

Типичные ошибки при работе с тегами

Одной из распространённых ошибок является отсутствие согласованности между providesTags и invalidatesTags. Например:

  • query предоставляет Post;
  • mutation инвалидирует Posts.

В таком случае совпадение не произойдёт, и рефетч не запустится.

Ещё одна ошибка — отсутствие id при динамическом тегировании, что приводит к невозможности точечной инвалидации.


Стратегия проектирования тегов

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

  • каждая сущность получает свой тип;
  • каждая коллекция имеет LIST-тег;
  • связи между сущностями отражаются через комбинированные теги;
  • мутации всегда явно описывают, что именно устаревает.

Такая структура делает кэш предсказуемым и управляемым без ручного вмешательства в refetch logic.