invalidatesTags в mutation endpoints

Свойство invalidatesTags используется в mutation endpoints для управления автоматической инвалидацией кэша. После успешного выполнения мутации RTK Query помечает связанные данные как устаревшие и инициирует повторное получение информации для всех query endpoints, которые предоставляют соответствующие теги через providesTags.

Механизм строится вокруг двух ключевых концепций:

  • providesTags — объявляет, какие данные предоставляет query endpoint
  • invalidatesTags — объявляет, какие данные становятся неактуальными после mutation endpoint

Без invalidatesTags кэш после мутаций остаётся неизменным. Это приводит к устаревшим данным в интерфейсе.


Базовый пример инвалидации

Query endpoint

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

export const postsApi = createApi({
    reducerPath: 'postsApi',
    baseQuery: fetchBaseQuery({
        baseUrl: '/api'
    }),
    tagTypes: ['Posts'],
    endpoints: (builder) => ({
        getPosts: builder.query({
            query: () => '/posts',
            providesTags: ['Posts']
        }),

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

Что происходит после мутации

Последовательность работы:

  1. Компонент вызывает useGetPostsQuery()
  2. RTK Query загружает список постов
  3. Результат сохраняется в кэш
  4. Endpoint getPosts помечает данные тегом Posts
  5. Выполняется createPost
  6. После успешной мутации RTK Query инвалидирует тег Posts
  7. Все query endpoints с providesTags: ['Posts'] автоматически перезапрашиваются

Автоматический рефетч

Главное преимущество invalidatesTags — автоматическая синхронизация состояния.

Без инвалидации:

createPost: builder.mutation({
    query: (body) => ({
        url: '/posts',
        method: 'POST',
        body
    })
})

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

С инвалидацией:

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

RTK Query автоматически обновит список.


Использование функции в invalidatesTags

invalidatesTags может быть функцией.

Сигнатура:

invalidatesTags: (result, error, arg) => []

Аргументы:

Аргумент Описание
result Ответ сервера
error Ошибка запроса
arg Аргумент мутации

Инвалидация после успешного запроса

Часто инвалидация должна происходить только при успешной операции.

updatePost: builder.mutation({
    query: ({ id, ...patch }) => ({
        url: `/posts/${id}`,
        method: 'PATCH',
        body: patch
    }),

    invalidatesTags: (result, error, arg) =>
        result
            ? ['Posts']
            : []
})

Если сервер вернул ошибку, повторный запрос не выполняется.


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

Инвалидация конкретного ресурса

updatePost: builder.mutation({
    query: ({ id, ...patch }) => ({
        url: `/posts/${id}`,
        method: 'PATCH',
        body: patch
    }),

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

Здесь инвалидируется только конкретный пост.


Теги с id

Точечная инвалидация

RTK Query поддерживает объектный формат тегов:

{
    type: 'Posts',
    id: 15
}

Это позволяет обновлять отдельные сущности вместо полного списка.


Связка providesTags и invalidatesTags

Query endpoint

getPost: builder.query({
    query: (id) => `/posts/${id}`,

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

Mutation endpoint

updatePost: builder.mutation({
    query: ({ id, ...patch }) => ({
        url: `/posts/${id}`,
        method: 'PATCH',
        body: patch
    }),

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

Преимущество точечной инвалидации

Полная инвалидация:

invalidatesTags: ['Posts']

Вызывает:

  • обновление всех списков
  • обновление всех отдельных постов
  • дополнительные сетевые запросы

Точечная инвалидация:

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

Обновляет только нужные данные.


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

Распространённый паттерн:

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

Используется, когда:

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

Например:

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

Инвалидация после удаления

Удаление сущности

deletePost: builder.mutation({
    query: (id) => ({
        url: `/posts/${id}`,
        method: 'DELETE'
    }),

    invalidatesTags: (result, error, id) => [
        { type: 'Posts', id },
        'Posts'
    ]
})

После удаления:

  • кэш конкретного поста очищается
  • список постов обновляется

Работа со списками

Типичный endpoint списка

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

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

Что даёт такой подход

Каждый элемент списка получает собственный тег.

Например:

[
    'Posts',
    { type: 'Posts', id: 1 },
    { type: 'Posts', id: 2 },
    { type: 'Posts', id: 3 }
]

Теперь можно:

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

Паттерн LIST

RTK Query часто использует специальный идентификатор LIST.

Query endpoint

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

    providesTags: (result = []) => [
        { type: 'Posts', id: 'LIST' },

        ...result.map(post => ({
            type: 'Posts',
            id: post.id
        }))
    ]
})

Для чего нужен LIST

LIST представляет весь список как отдельную сущность.

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

  • отдельно инвалидировать список
  • отдельно инвалидировать элементы
  • уменьшать количество запросов

Добавление новой записи

createPost: builder.mutation({
    query: (body) => ({
        url: '/posts',
        method: 'POST',
        body
    }),

    invalidatesTags: [
        { type: 'Posts', id: 'LIST' }
    ]
})

Новая запись влияет именно на список, а не на отдельные сущности.


Обновление существующей записи

updatePost: builder.mutation({
    query: ({ id, ...patch }) => ({
        url: `/posts/${id}`,
        method: 'PATCH',
        body: patch
    }),

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

Список при этом не обновляется.


Удаление записи

Удаление почти всегда влияет и на список, и на сущность.

deletePost: builder.mutation({
    query: (id) => ({
        url: `/posts/${id}`,
        method: 'DELETE'
    }),

    invalidatesTags: (result, error, id) => [
        { type: 'Posts', id },
        { type: 'Posts', id: 'LIST' }
    ]
})

Несколько типов тегов

RTK Query поддерживает несколько tag types.

tagTypes: ['Posts', 'Users', 'Comments']

Инвалидация нескольких тегов

invalidatesTags: ['Posts', 'Comments']

Или:

invalidatesTags: [
    { type: 'Posts', id: 10 },
    { type: 'Comments', id: 55 }
]

Инвалидация связанных данных

Например, после создания комментария:

createComment: builder.mutation({
    query: (body) => ({
        url: '/comments',
        method: 'POST',
        body
    }),

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

Обновятся:

  • комментарии поста
  • данные поста
  • счётчики комментариев

Условная инвалидация

Инвалидация может зависеть от ответа сервера.

invalidatesTags: (result) => {
    if (!result) {
        return []
    }

    if (result.status === 'published') {
        return ['Posts']
    }

    return []
}

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

invalidatesTags: (result) => [
    {
        type: 'Posts',
        id: result.id
    }
]

Это удобно, когда сервер генерирует идентификатор самостоятельно.


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

Отсутствие tagTypes

createApi({
    reducerPath: 'api',
    baseQuery,
    endpoints: () => ({})
})

Без:

tagTypes: ['Posts']

Теги работать не будут.


Несовпадение имён тегов

Ошибка:

providesTags: ['Post']
invalidatesTags: ['Posts']

Теги должны совпадать полностью.


Инвалидация без providesTags

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

Если query endpoint не предоставляет теги, инвалидировать нечего.


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

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

invalidatesTags: ['Posts']

для любой операции.

Это приводит к:

  • лишним запросам
  • лишним рендерам
  • дополнительной нагрузке

Оптимальная стратегия

Операция Инвалидация
Создание LIST
Обновление конкретный id
Удаление id + LIST
Массовое изменение весь тип

Взаимодействие с optimistic updates

Даже при использовании updateQueryData инвалидация остаётся полезной.

async onQueryStarted(arg, { dispatch, queryFulfilled }) {
    const patchResult = dispatch(
        api.util.updateQueryData(
            'getPost',
            arg.id,
            draft => {
                draft.title = arg.title
            }
        )
    )

    try {
        await queryFulfilled
    } catch {
        patchResult.undo()
    }
}

И дополнительно:

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

Оптимистичное обновление мгновенно меняет интерфейс, а инвалидация гарантирует синхронизацию с сервером.


Инвалидация и polling

Если endpoint использует polling:

useGetPostsQuery(undefined, {
    pollingInterval: 5000
})

Инвалидация всё равно вызывает немедленный рефетч, независимо от таймера polling.


Инвалидация и keepUnusedDataFor

Даже если кэш ещё не удалён:

keepUnusedDataFor: 60

Инвалидация пометит данные устаревшими и обновит активные подписки.


Инвалидация inactive queries

RTK Query обновляет только активные подписки.

Если query endpoint сейчас не используется компонентами:

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

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


Полный пример API

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

export const postsApi = createApi({
    reducerPath: 'postsApi',

    baseQuery: fetchBaseQuery({
        baseUrl: '/api'
    }),

    tagTypes: ['Posts'],

    endpoints: (builder) => ({
        getPosts: builder.query({
            query: () => '/posts',

            providesTags: (result = []) => [
                { type: 'Posts', id: 'LIST' },

                ...result.map(post => ({
                    type: 'Posts',
                    id: post.id
                }))
            ]
        }),

        getPost: builder.query({
            query: (id) => `/posts/${id}`,

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

        createPost: builder.mutation({
            query: (body) => ({
                url: '/posts',
                method: 'POST',
                body
            }),

            invalidatesTags: [
                { type: 'Posts', id: 'LIST' }
            ]
        }),

        updatePost: builder.mutation({
            query: ({ id, ...patch }) => ({
                url: `/posts/${id}`,
                method: 'PATCH',
                body: patch
            }),

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

        deletePost: builder.mutation({
            query: (id) => ({
                url: `/posts/${id}`,
                method: 'DELETE'
            }),

            invalidatesTags: (result, error, id) => [
                { type: 'Posts', id },
                { type: 'Posts', id: 'LIST' }
            ]
        })
    })
})