Стратегии инвалидации кэша

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

Ключевая идея заключается в том, что запросы не инвалидируются вручную в классическом императивном стиле. Вместо этого описывается связь: какие данные предоставляет endpoint и какие данные он затрагивает при мутациях.


Теги как основа инвалидации

RTK Query использует механизм providesTags и invalidatesTags.

  • providesTags — описывает, какие сущности формирует запрос
  • invalidatesTags — описывает, какие сущности изменяет мутация

Базовый пример зависимости

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

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

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


Структурированные теги и точечная инвалидация

Простые строки в тегах подходят только для глобальной инвалидации. Более точный контроль достигается через объектные теги с type и id.

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

Такой подход формирует два уровня кэш-сущностей:

  • отдельные посты (Post/1, Post/2)
  • список (Post/LIST)

Точечная инвалидация сущностей

При использовании структурированных тегов появляется возможность инвалидировать только изменённую сущность.

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

Результат — обновляется только конкретный пост, без повторного запроса всего списка.


Инвалидация списка через LIST-тег

Список часто рассматривается как отдельная сущность. При добавлении или удалении элементов он должен обновляться целиком.

deletePost: builder.mutation({
  query: (id) => ({
    url: `/posts/${id}`,
    method: 'DELETE'
  }),
  invalidatesTags: [
    { type: 'Post', id: 'LIST' }
  ]
})

Использование LIST позволяет отделить операции над коллекцией от операций над отдельными сущностями.


Комбинированная стратегия инвалидации

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

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

Такой подход применяется, когда:

  • изменяется сущность
  • и одновременно может меняться порядок, фильтрация или агрегированные данные списка

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

RTK Query позволяет динамически формировать теги в зависимости от ответа сервера.

updateUser: builder.mutation({
  query: (user) => ({
    url: `/users/${user.id}`,
    method: 'PUT',
    body: user
  }),
  invalidatesTags: (result) => {
    if (!result) return []

    return [
      { type: 'User', id: result.id },
      result.isAdmin && { type: 'AdminStats', id: 'SUMMARY' }
    ].filter(Boolean)
  }
})

Такой подход позволяет учитывать бизнес-логику при выборе того, какие части кэша должны быть обновлены.


Инвалидация нескольких типов сущностей

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

createComment: builder.mutation({
  query: (comment) => ({
    url: '/comments',
    method: 'POST',
    body: comment
  }),
  invalidatesTags: [
    { type: 'Comment', id: 'LIST' },
    { type: 'Post', id: comment.postId }
  ]
})

Здесь происходит синхронное обновление:

  • списка комментариев
  • конкретного поста, к которому привязан комментарий

Перекрёстная инвалидация между сущностями

Связанные сущности часто требуют каскадного обновления.

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

Такая модель используется, когда один и тот же ресурс может отображаться в разных представлениях (список, детальная карточка, превью).


Инвалидация без тегов

В некоторых случаях теги не используются, и обновление происходит через refetch вручную или через refetchOnMountOrArgChange.

getPosts: builder.query({
  query: () => '/posts',
  refetchOnMountOrArgChange: true
})

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


Принудительная инвалидация через dispatch

RTK Query предоставляет API для ручного управления кэшем через dispatch.

dispatch(api.util.invalidateTags([{ type: 'Post', id: 'LIST' }]))

Использование такого механизма характерно для:

  • событий вне RTK Query (WebSocket, SSE)
  • глобальных обновлений данных
  • синхронизации после фоновых операций

Инвалидация в реальном времени

При интеграции с WebSocket или polling модель инвалидации становится событийной.

socket.on('postUpdated', (post) => {
  dispatch(
    api.util.invalidateTags([{ type: 'Post', id: post.id }])
  )
})

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


Оптимизация массовой инвалидации

Чрезмерное использование глобальных тегов приводит к лишним рефетчам. Для оптимизации применяются:

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

Плохой вариант:

invalidatesTags: ['Post']

Хороший вариант:

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

Кэш как граф зависимостей

Модель RTK Query можно рассматривать как ориентированный граф:

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

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


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

Частые проблемы при работе с инвалидацией:

  • использование только строковых тегов без структуры
  • отсутствие разделения LIST и entity
  • чрезмерная инвалидизация всего кэша
  • дублирование тегов между разными endpoint’ами
  • несогласованность типов (Post vs Posts)

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


Согласованность тегов между endpoint’ами

Критически важно поддерживать единый реестр типов тегов в рамках API slice.

tagTypes: ['Post', 'User', 'Comment']

Несовпадение имен приводит к тому, что инвалидация перестаёт работать, несмотря на корректную структуру кода.