Инвалидация кэша после мутации

Базовая модель кэширования RTK Query

RTK Query строит систему кэширования вокруг двух ключевых понятий: query (запрос данных) и mutation (изменение данных). Каждый query сохраняет результат в кэше Redux Toolkit и повторно использует его до тех пор, пока не наступит одно из условий обновления.

Кэш в RTK Query не является статичным. Он управляется через:

  • время жизни данных (garbage collection),
  • параметры запроса,
  • ручное обновление,
  • и ключевой механизм — инвалидацию тегов (tag invalidation).

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


Концепция тегов (tags)

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

Основные идеи:

  • query помечает данные тегами через providesTags
  • mutation сигнализирует, какие теги стали неактуальными через invalidatesTags
  • RTK Query автоматически решает, какие запросы нужно перезапросить

Пример логики:

  • список пользователей → тег User
  • обновление пользователя → инвалидирует User
  • список пользователей автоматически перезапрашивается

Определение providesTags в query

providesTags описывает, какие сущности «предоставляет» запрос.

getUsers: builder.query({
  query: () => '/users',
  providesTags: ['User']
})

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

Более детализированный вариант — тегирование по id:

getUsers: builder.query({
  query: () => '/users',
  providesTags: (result) =>
    result
      ? [
          ...result.map(user => ({ type: 'User', id: user.id })),
          { type: 'User', id: 'LIST' }
        ]
      : [{ type: 'User', id: 'LIST' }]
})

Здесь появляются два уровня:

  • User/LIST — общий список
  • User/:id — конкретные сущности

Инвалидация через invalidatesTags

Мутация сообщает RTK Query, какие данные устарели.

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

После успешного выполнения:

  • тег User/:id считается устаревшим
  • все query, которые его предоставляют, автоматически обновляются

Связь query и mutation через теги

Механизм работает по следующей цепочке:

  1. query регистрирует теги через providesTags
  2. mutation помечает теги через invalidatesTags
  3. RTK Query сравнивает наборы тегов
  4. совпадающие запросы получают сигнал на refetch

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

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

updateUser: builder.mutation({
  query: (user) => ({
    url: `/users/${user.id}`,
    method: 'PATCH',
    body: user
  }),
  invalidatesTags: (result, error, user) => [
    { type: 'User', id: user.id }
  ]
})

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

  • конкретный запрос getUserById(id) пересчитывается
  • остальные запросы остаются нетронутыми

Инвалидация списков и ключ LIST

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

getUsers: builder.query({
  query: () => '/users',
  providesTags: (result) =>
    result
      ? [
          { type: 'User', id: 'LIST' },
          ...result.map(u => ({ type: 'User', id: u.id }))
        ]
      : [{ type: 'User', id: 'LIST' }]
})

Мутация добавления пользователя:

addUser: builder.mutation({
  query: (user) => ({
    url: '/users',
    method: 'POST',
    body: user
  }),
  invalidatesTags: [{ type: 'User', id: 'LIST' }]
})

Смысл:

  • список пользователей устаревает
  • RTK Query перезапрашивает именно список
  • индивидуальные запросы остаются актуальными

Частичная и точечная инвалидация

Инвалидация может быть:

1. Полной (по типу)
invalidatesTags: [{ type: 'User' }]

Используется редко, так как приводит к массовому рефетчу.

2. Частичной (по id)
invalidatesTags: [{ type: 'User', id: 5 }]

Оптимальный вариант для большинства CRUD операций.

3. Смешанной
invalidatesTags: [
  { type: 'User', id: 'LIST' },
  { type: 'Stats', id: 'LIST' }
]

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


Автоматический refetch и его поведение

После инвалидирования RTK Query:

  • определяет затронутые cache entries
  • помечает их как stale
  • запускает повторный запрос при необходимости

Поведение зависит от:

  • активных подписчиков (component usage)
  • настроек refetchOnMountOrArgChange
  • наличия keepUnusedDataFor

Если запрос не используется, он может быть удалён из кэша без refetch.


Инвалидация при optimistic update

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

updateUser: builder.mutation({
  query: (user) => ({
    url: `/users/${user.id}`,
    method: 'PATCH',
    body: user
  }),
  async onQueryStarted(user, { dispatch, queryFulfilled }) {
    const patch = dispatch(
      api.util.updateQueryData('getUserById', user.id, (draft) => {
        Object.assign(draft, user)
      })
    )

    try {
      await queryFulfilled
    } catch {
      patch.undo()
    }
  },
  invalidatesTags: (result, error, user) => [
    { type: 'User', id: user.id }
  ]
})

Здесь происходит двойная синхронизация:

  • локальное обновление cache
  • последующая инвалидация для подтверждения серверного состояния

Гранулярность тегов и архитектурные решения

Чем точнее разметка тегов, тем эффективнее работа кэша.

Сравнение подходов:

Грубая стратегия
providesTags: ['User']
invalidatesTags: ['User']

Минусы:

  • лишние запросы
  • отсутствие контроля
Средняя стратегия
{ type: 'User', id: 'LIST' }
{ type: 'User', id }

Баланс между производительностью и простотой.

Точная стратегия
{ type: 'User', id }
{ type: 'User', id: 'profile' }
{ type: 'User', id: 'permissions' }

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


Инвалидация в связных сущностях

В реальных приложениях данные часто связаны:

  • пользователи
  • роли
  • комментарии
  • статистика

Пример каскадной инвалидации:

invalidatesTags: [
  { type: 'User', id: arg.id },
  { type: 'Post', id: 'LIST' },
  { type: 'Comment', id: 'LIST' }
]

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


Ошибки при использовании invalidation

Типовые проблемы:

1. Отсутствие providesTags

Если query не объявляет providesTags, инвалидация не сработает.

2. Несовпадение типов тегов
providesTags: ['Users']
invalidatesTags: ['User']

Разные строки — разные сущности.

3. Избыточная инвалидация

Инвалидация всего типа:

{ type: 'User' }

приводит к массовым refetch и деградации производительности.


Поведение кэша после инвалидации

После пометки тега как устаревшего:

  • активные query становятся stale
  • запускается refetch при наличии подписчиков
  • устаревшие данные остаются в памяти до перезаписи
  • неиспользуемые query удаляются по GC таймеру

Практическая модель проектирования тегов

Рекомендуемая структура:

  • сущность: User, Post, Comment
  • список: LIST
  • сущность по id: {type, id}
  • дополнительные сегменты: profile, settings, stats

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

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

Итоговое понимание механизма

Инвалидация в RTK Query — это не ручное управление кешем, а декларативная система синхронизации данных.

Она строится на трёх уровнях:

  • описание данных через providesTags
  • сигнал изменения через invalidatesTags
  • автоматический рефетч движком RTK Query

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