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

Условная инвалидация в RTK Query — механизм выборочного сброса кэша, при котором обновление данных выполняется только при соблюдении определённых условий. В отличие от безусловной инвалидации, где запросы автоматически помечаются устаревшими после каждой mutation-операции, условный подход позволяет контролировать процесс обновления значительно точнее.

Такой механизм особенно важен в крупных приложениях, где:

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

RTK Query предоставляет несколько способов реализации условной инвалидации:

  • вычисляемые invalidatesTags;
  • динамические providesTags;
  • ручная инвалидация через api.util.invalidateTags;
  • проверка результатов mutation;
  • анализ аргументов запросов;
  • использование lifecycle-обработчиков;
  • оптимистичные обновления без инвалидации.

Базовая схема условной инвалидации

Обычная инвалидация выглядит так:

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

    invalidatesTags: ['Post']
})

После выполнения mutation RTK Query инвалидирует все запросы с тегом Post.

Условная инвалидация позволяет определить логику динамически:

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

    invalidatesTags: (result, error, arg) => {
        if (error) {
            return []
        }

        return ['Post']
    }
})

Если mutation завершилась ошибкой — инвалидация не выполняется.


Аргументы функции invalidatesTags

Функция получает три параметра:

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

result

Содержит успешный ответ сервера.

invalidatesTags: (result) => {
    console.log(result)

    return ['Post']
}

Пример ответа:

{
    id: 15,
    title: 'New title',
    published: true
}

error

Содержит объект ошибки.

invalidatesTags: (result, error) => {
    if (error) {
        console.error(error)
    }

    return []
}

arg

Аргументы mutation-запроса.

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

    invalidatesTags: (result, error, arg) => {
        console.log(arg)

        return ['Post']
    }
})

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

Наиболее распространённый сценарий.

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

    return ['Post']
}

Проблема безусловной инвалидации заключается в том, что даже при ошибке сервера RTK Query всё равно обновляет связанные query-запросы.

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

  • лишним запросам;
  • ненужным перерендерам;
  • дополнительной нагрузке на API;
  • визуальным скачкам интерфейса.

Условная проверка устраняет проблему.


Инвалидация в зависимости от данных ответа

Сервер может вернуть информацию о фактических изменениях.

Пример:

{
    success: true,
    updated: false
}

Инвалидация выполняется только при реальном обновлении:

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

    return ['Post']
}

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

Часто требуется обновлять кэш только при определённом статусе сущности.

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

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

        return ['Post']
    }
})

Черновики не вызывают обновление списка опубликованных материалов.


Проверка аргументов mutation

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

invalidatesTags: (result, error, arg) => {
    if (arg.skipCache) {
        return []
    }

    return ['Post']
}

Вызов:

updatePost({
    id: 1,
    title: 'New title',
    skipCache: true
})

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

Одна из важнейших возможностей RTK Query — выборочное обновление отдельных записей.

Безусловная инвалидация

invalidatesTags: ['Post']

Обновляются:

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

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

invalidatesTags: (result, error, arg) => {
    if (error) {
        return []
    }

    return [
        {
            type: 'Post',
            id: arg.id
        }
    ]
}

Теперь обновляется только конкретный пост.


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

Часто изменение записи влияет и на список.

invalidatesTags: (result, error, arg) => {
    if (error) {
        return []
    }

    return [
        { type: 'Post', id: arg.id },
        { type: 'Post', id: 'LIST' }
    ]
}

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

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

Query

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

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

Mutation

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

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

        return [
            { type: 'Post', id: 'LIST' }
        ]
    }
})

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


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

Mutation может выполнять разные действия.

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

    invalidatesTags: (result, error, arg) => {
        if (arg.action === 'view') {
            return []
        }

        if (arg.action === 'like') {
            return [
                { type: 'Post', id: arg.id }
            ]
        }

        return ['Post']
    }
})

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

Удаление требует отдельного подхода.

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

    invalidatesTags: (result, error, id) => {
        if (error) {
            return []
        }

        return [
            { type: 'Post', id },
            { type: 'Post', id: 'LIST' }
        ]
    }
})

Использование api.util.invalidateTags

RTK Query позволяет инвалидировать кэш вручную.

dispatch(
    api.util.invalidateTags([
        'Post'
    ])
)

Главное преимущество — возможность выполнить проверку перед обновлением.


Условная ручная инвалидация

const handleSave = async () => {
    const result = await updatePost(data)

    if ('data' in result) {
        dispatch(
            api.util.invalidateTags([
                'Post'
            ])
        )
    }
}

Инвалидация внутри onQueryStarted

Lifecycle-обработчик предоставляет полный контроль.

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

    async onQueryStarted(arg, api) {
        try {
            await api.queryFulfilled

            api.dispatch(
                apiSlice.util.invalidateTags([
                    {
                        type: 'Post',
                        id: arg.id
                    }
                ])
            )
        } catch (error) {

        }
    }
})

Комбинация optimistic update и условной инвалидации

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

Оптимистичное обновление

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

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

Инвалидация только при рассинхронизации

invalidatesTags: (result, error) => {
    if (error) {
        return ['Post']
    }

    return []
}

Такой подход:

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

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

Пример сложной бизнес-логики.

invalidatesTags: (result, error, arg) => {
    if (arg.userRole !== 'admin') {
        return []
    }

    return ['Users']
}

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

Некоторые API возвращают состояние синхронизации.

{
    synced: true
}
invalidatesTags: (result) => {
    if (!result?.synced) {
        return []
    }

    return ['Settings']
}

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

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

    if (arg.updatePost) {
        tags.push('Post')
    }

    if (arg.updateUser) {
        tags.push('User')
    }

    if (arg.updateComments) {
        tags.push('Comment')
    }

    return tags
}

Инвалидация в зависимости от HTTP-кода

RTK Query позволяет анализировать структуру ошибки.

invalidatesTags: (result, error) => {
    if (error?.status === 409) {
        return ['Post']
    }

    return []
}

Инвалидация после пакетных операций

Массовые обновления требуют отдельного подхода.

bulkUpdatePosts: builder.mutation({
    query: (ids) => ({
        url: 'posts/bulk',
        method: 'PATCH',
        body: ids
    }),

    invalidatesTags: (result, error, ids) => {
        if (error) {
            return []
        }

        return ids.map(id => ({
            type: 'Post',
            id
        }))
    }
})

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

Изменение одной сущности может затрагивать другие.

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

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

        return [
            {
                type: 'Comment',
                id: result.id
            },
            {
                type: 'Post',
                id: result.postId
            }
        ]
    }
})

Динамическая генерация тегов

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

    return result.affectedIds.map(id => ({
        type: 'Post',
        id
    }))
}

Ошибки условной инвалидации

Возврат undefined

Неправильно:

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

    return ['Post']
}

Правильно:

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

    return ['Post']
}

Безусловная инвалидация LIST

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

Даже минимальное изменение вызывает полную перезагрузку списка.


Инвалидация слишком большого количества данных

Плохо:

return ['Post', 'User', 'Comment', 'Profile']

Это приводит к каскадным запросам.


Стратегии оптимизации

Минимальная область инвалидации

Всегда предпочтительно инвалидировать:

{ type: 'Post', id }

вместо:

['Post']

Комбинация updateQueryData и invalidateTags

Оптимальный подход:

  1. локально обновить кэш;
  2. избежать refetch;
  3. выполнить инвалидацию только при ошибке.

Разделение LIST и ITEM

{ type: 'Post', id: 'LIST' }

и

{ type: 'Post', id: 15 }

должны использоваться отдельно.


Архитектурные подходы

Централизованная схема тегов

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

Константы для LIST

const LIST_ID = 'LIST'

Хелперы инвалидации

const invalidatePost = (id) => [
    { type: 'Post', id }
]

Пример полноценной условной инвалидации

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

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

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

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

    invalidatesTags: (result, error, arg) => {
        if (error) {
            return []
        }

        if (result?.requiresFullReload) {
            return [
                { type: 'Post', id: 'LIST' }
            ]
        }

        return [
            {
                type: 'Post',
                id: arg.id
            }
        ]
    }
})

Такая схема сочетает:

  • оптимистичное обновление;
  • откат при ошибке;
  • точечную инвалидацию;
  • условительный refetch;
  • минимальную нагрузку на API;
  • высокую производительность интерфейса.