providesTags в query endpoints

В RTK Query система кэширования строится вокруг ключей запросов и тегов, которые позволяют связывать полученные данные с последующей инвалидацией. В query endpoints параметр providesTags определяет, какие логические теги “помечают” результат запроса после его выполнения. Эти теги затем используются мутациями для точечного обновления кэша без необходимости вручную управлять состоянием.


Базовая идея providesTags

Каждый query endpoint может “предоставлять” один или несколько тегов. Эти теги описывают, какие сущности представлены в ответе.

Основная цель:

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

Подключение tagTypes

Перед использованием providesTags необходимо объявить типы тегов на уровне API:

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

export const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  tagTypes: ['Post', 'User'],
  endpoints: (builder) => ({
    // endpoints here
  }),
});

tagTypes задаёт список допустимых категорий тегов. Любой тег вне этого списка не будет участвовать в системе инвалидации.


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

Самый прямой вариант — вернуть фиксированный набор тегов:

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

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

Недостаток этого подхода заключается в отсутствии гранулярности. Любая мутация с тегом Post приведёт к рефетчу всего списка.


Объектная форма providesTags

Более гибкий вариант — возвращать массив объектов:

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

Здесь появляется концепция виртуального идентификатора LIST, который обычно используется для списков.

Такой подход позволяет разделять:

  • список сущностей (LIST)
  • отдельные элементы (id)

Динамическое формирование тегов из результата

Наиболее распространённый вариант — генерация тегов на основе ответа API:

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

Здесь происходит следующее:

  • каждый пост получает свой тег { type: 'Post', id }
  • дополнительно добавляется общий тег списка { id: 'LIST' }
  • при отсутствии результата возвращается только список

Поведение при пустом или ошибочном ответе

Функция providesTags получает не только result, но также может учитывать состояние:

providesTags: (result, error, arg) => {
  if (error) return [];
  if (!result) return [{ type: 'Post', id: 'LIST' }];

  return [
    ...result.map((item) => ({ type: 'Post', id: item.id })),
    { type: 'Post', id: 'LIST' },
  ];
}

Такой вариант предотвращает загрязнение кэша при ошибках запроса.


Использование аргументов запроса

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

getPostsByUser: builder.query({
  query: (userId) => `/users/${userId}/posts`,
  providesTags: (result, error, userId) =>
    result
      ? [
          ...result.map((post) => ({
            type: 'Post',
            id: post.id,
          })),
          { type: 'Post', id: `USER_${userId}` },
        ]
      : [{ type: 'Post', id: `USER_${userId}` }],
});

В этом случае кэш становится сегментированным по пользователям.


Связь providesTags и invalidatesTags

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

Обновление происходит через мутации:

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

После успешной мутации все query endpoints, предоставляющие тег { type: 'Post', id: 'LIST' }, автоматически перезапрашиваются.


Гранулярная инвалидация отдельных сущностей

Если query возвращает посты с индивидуальными тегами:

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

Тогда мутация может инвалидировать только один элемент:

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

Это позволяет избежать полного рефетча списка.


Частые шаблоны использования

Список + элементы

providesTags: (result) =>
  result
    ? [
        ...result.map(({ id }) => ({ type: 'Post', id })),
        { type: 'Post', id: 'LIST' },
      ]
    : [{ type: 'Post', id: 'LIST' }];

Используется для коллекций, где важны как отдельные элементы, так и весь список.


Только список

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

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


Сегментация по параметрам

providesTags: (result, error, category) => [
  { type: 'Post', id: `CATEGORY_${category}` },
];

Используется при фильтрации данных по категориям.


Поведение кэша при совпадении тегов

RTK Query хранит результаты запросов в нормализованном виде по ключу endpoint + аргументы. Теги добавляют дополнительный уровень связи:

  • один тег может быть связан с множеством query cache entries
  • одна мутация может инвалидировать множество запросов
  • пересечение тегов определяет, какие запросы будут перезапрошены

Оптимизация количества тегов

Избыточное количество тегов приводит к росту памяти и усложнению инвалидации. Обычно используется баланс:

  • один тег на сущность (id)
  • один тег на коллекцию (LIST)
  • дополнительные теги только при необходимости сегментации

Ошибки проектирования providesTags

Распространённые проблемы:

  • отсутствие LIST тега, что усложняет обновление списков
  • генерация тегов без стабильных id
  • использование случайных значений в тегах
  • отсутствие синхронизации между providesTags и invalidatesTags

Влияние на перезапросы

При срабатывании инвалидации RTK Query:

  • ищет все query, которые “предоставляют” указанный тег
  • помечает их как устаревшие
  • автоматически инициирует refetch при необходимости (в зависимости от подписки компонентов)

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

Один endpoint может возвращать разные типы тегов одновременно:

providesTags: (result, error, arg) => {
  const baseTags = [{ type: 'Post', id: 'LIST' }];

  const userTags = result
    ? result.map((p) => ({ type: 'User', id: p.userId }))
    : [];

  return [...baseTags, ...userTags];
};

Такой подход используется, когда один endpoint влияет на несколько сущностей.


Итоговая модель поведения providesTags

Механизм можно представить как трёхуровневую систему:

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

Эта модель позволяет строить предсказуемую и масштабируемую систему синхронизации клиентского состояния с сервером без ручного управления кэшем.