From harness-claude
Apply optimistic and pessimistic cache updates using onQueryStarted in RTK Query mutations for instant UI feedback and automatic rollback on failure.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Apply optimistic and pessimistic cache updates with onQueryStarted for instant UI feedback with automatic rollback
Implements optimistic updates in TanStack Query: snapshot cache before mutations, apply immediate UI changes, rollback on errors, invalidate on settle. For responsive toggles, likes, reorders despite latency.
Provides patterns and code examples for Relay mutations in React/GraphQL apps, including optimistic updates, connections, declarative mutations, and error handling.
Provides expertise in TanStack Query for React/Next.js data fetching, caching (staleTime/gcTime), mutations, optimistic updates, cache invalidation, and App Router SSR hydration.
Share bugs, ideas, or general feedback.
Apply optimistic and pessimistic cache updates with onQueryStarted for instant UI feedback with automatic rollback
onQueryStarted in the mutation endpoint to perform cache updates before or after the server responds.dispatch(api.util.updateQueryData(...)) immediately inside onQueryStarted, before awaiting the result. Save the return value — it contains an undo function.await queryFulfilled in try/catch. On failure, call patchResult.undo() to revert the optimistic change.await queryFulfilled first, then update the cache with the server's response.updateQueryData — a mismatch silently does nothing.// Optimistic update — toggle a todo's completed status
toggleTodo: builder.mutation<Todo, { id: string; completed: boolean }>({
query: ({ id, completed }) => ({
url: `/todos/${id}`,
method: 'PATCH',
body: { completed },
}),
async onQueryStarted({ id, completed }, { dispatch, queryFulfilled }) {
// Optimistically update the cache immediately
const patchResult = dispatch(
api.util.updateQueryData('getTodos', undefined, (draft) => {
const todo = draft.find((t) => t.id === id);
if (todo) todo.completed = completed;
})
);
try {
await queryFulfilled;
} catch {
// Revert on failure
patchResult.undo();
}
},
}),
// Pessimistic update — server assigns the ID
createTodo: builder.mutation<Todo, { title: string }>({
query: (body) => ({ url: '/todos', method: 'POST', body }),
async onQueryStarted(_, { dispatch, queryFulfilled }) {
try {
const { data: newTodo } = await queryFulfilled;
// Update cache with server response
dispatch(
api.util.updateQueryData('getTodos', undefined, (draft) => {
draft.push(newTodo);
})
);
} catch {
// No cache to revert — the mutation failed before we touched it
}
},
}),
updateQueryData arguments: updateQueryData(endpointName, queryArg, updateFn). The queryArg must exactly match what was passed to the query hook. If the query was called with useGetTodosQuery(undefined), pass undefined. If called with useGetTodosQuery({ filter: 'active' }), pass { filter: 'active' }.
Combining with invalidation: You can use optimistic updates AND invalidatesTags together. The optimistic update gives instant feedback, and the invalidation ensures the cache is eventually consistent with the server.
Multiple cache entries: If the same data appears in multiple queries (a list query and a detail query), update both:
async onQueryStarted({ id, title }, { dispatch, queryFulfilled }) {
const patchList = dispatch(api.util.updateQueryData('getPosts', undefined, (draft) => {
const post = draft.find((p) => p.id === id);
if (post) post.title = title;
}));
const patchDetail = dispatch(api.util.updateQueryData('getPost', id, (draft) => {
draft.title = title;
}));
try {
await queryFulfilled;
} catch {
patchList.undo();
patchDetail.undo();
}
},
Anti-patterns:
undo() on failure — leaves the UI in a stale statehttps://redux-toolkit.js.org/rtk-query/usage/manual-cache-updates