Оптимистичные обновления в RTK Query основаны на предположении, что сервер вернёт успешный результат, и позволяют мгновенно отражать изменения в интерфейсе до фактического ответа API. Механизм строится на управляемом изменении кэша и последующем подтверждении или откате состояния в зависимости от результата запроса.
Суть подхода заключается в предварительном изменении локального кэша RTK Query перед завершением мутации. В момент вызова запроса состояние данных обновляется немедленно, создавая ощущение мгновенной реакции интерфейса. Далее выполняется запрос к серверу, и результат либо подтверждает изменения, либо приводит к их откату.
Ключевые элементы механизма:
updateQueryDataonQueryStartedpatchResult.undo()RTK Query предоставляет хук onQueryStarted, который
позволяет перехватить момент старта запроса и синхронно изменить
кэш.
Процесс выглядит следующим образом:
Оптимистичное обновление чаще всего реализуется через
api.util.updateQueryData.
const api = createApi({
reducerPath: 'api',
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: (builder) => ({
getPosts: builder.query({
query: () => '/posts',
}),
addPost: builder.mutation({
query: (newPost) => ({
url: '/posts',
method: 'POST',
body: newPost,
}),
async onQueryStarted(newPost, { dispatch, queryFulfilled }) {
const patchResult = dispatch(
api.util.updateQueryData('getPosts', undefined, (draft) => {
draft.push({
id: 'temp-id',
...newPost,
});
})
);
try {
const { data: createdPost } = await queryFulfilled;
dispatch(
api.util.updateQueryData('getPosts', undefined, (draft) => {
const index = draft.findIndex((p) => p.id === 'temp-id');
if (index !== -1) {
draft[index] = createdPost;
}
})
);
} catch {
patchResult.undo();
}
},
}),
}),
});
updateQueryData возвращает объект patch, содержащий
возможность отмены изменений через undo.
Это позволяет строить предсказуемую модель:
Ключевая особенность заключается в том, что RTK Query использует immutable update внутри Immer, поэтому изменения описываются как мутация draft-состояния, но фактически остаются неизменяемыми.
При создании сущностей часто возникает проблема отсутствия ID до ответа сервера. Для решения используется временный идентификатор.
Типичный подход:
temp-id или UUID на клиентеimport { nanoid } from '@reduxjs/toolkit';
async onQueryStarted(newPost, { dispatch, queryFulfilled }) {
const tempId = nanoid();
const patchResult = dispatch(
api.util.updateQueryData('getPosts', undefined, (draft) => {
draft.push({ id: tempId, ...newPost });
})
);
try {
const { data } = await queryFulfilled;
dispatch(
api.util.updateQueryData('getPosts', undefined, (draft) => {
const post = draft.find((p) => p.id === tempId);
if (post) {
Object.assign(post, data);
}
})
);
} catch {
patchResult.undo();
}
}
Если запрос зависит от аргументов (фильтры, пагинация), необходимо учитывать ключ кэша.
api.util.updateQueryData('getPosts', { page: 1 }, (draft) => {
draft.items.unshift(newItem);
});
Каждая комбинация аргументов формирует отдельный кэш-слот, и обновление должно быть точечно направлено в нужный экземпляр.
Одна мутация может влиять на несколько запросов одновременно:
В таких случаях выполняется несколько updateQueryData
подряд.
dispatch(api.util.updateQueryData('getPosts', undefined, (draft) => {
draft.push(createdPost);
}));
dispatch(api.util.updateQueryData('getPostById', createdPost.id, () => {
return createdPost;
}));
Откат обеспечивается через patchResult.undo(). Это
критический механизм поддержания консистентности.
try {
await queryFulfilled;
} catch {
patchResult.undo();
}
Откат возвращает кэш в состояние до применения optimistic patch, что исключает необходимость ручного восстановления данных.
Оптимистичные обновления часто комбинируются с тегами
providesTags и invalidatesTags.
Однако при агрессивном использовании optimistic update:
Пример конфликтного сценария:
Для предотвращения используется:
updateQueryDataПри работе с вложенными данными используется мутация draft-объекта.
api.util.updateQueryData('getBoard', boardId, (draft) => {
const column = draft.columns.find(c => c.id === columnId);
column.cards.push(newCard);
});
Immer позволяет безопасно изменять глубокие структуры без ручного клонирования.
Каждое изменение кэша вызывает уведомление подписчиков. Поэтому важно:
Оптимистичные обновления становятся дорогими при избыточном обновлении крупных коллекций.
Возможны ситуации расхождения:
Стратегии обработки:
transformResponse может использоваться для нормализации
данных до попадания в кэш, что упрощает дальнейшие optimistic
операции.
transformResponse: (response) => {
return response.items;
}
После нормализации обновление кэша становится предсказуемым и не зависит от структуры API.
Чёткое разделение:
RTK Query не хранит отдельного слоя optimistic state, он реализуется через временные патчи кэша, что упрощает архитектуру и исключает дублирование состояния.
При быстром последовательном выполнении мутаций возможны гонки:
Решение:
В сценариях типа “перемещение карточки между колонками” требуется:
api.util.updateQueryData('getBoard', boardId, (draft) => {
const from = draft.columns.find(c => c.id === fromId);
const to = draft.columns.find(c => c.id === toId);
const cardIndex = from.cards.findIndex(c => c.id === cardId);
const [card] = from.cards.splice(cardIndex, 1);
to.cards.push(card);
});
Такие операции полностью выполняются на уровне кэша до обращения к серверу.
RTK Query формирует строгую и предсказуемую модель:
updateQueryDataqueryFulfilledundo при ошибкеЭта модель позволяет строить интерфейсы с мгновенной реакцией без усложнения глобального состояния приложения.