Skip to content
98 changes: 98 additions & 0 deletions docs/rtk-query/usage/manual-cache-updates.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,104 @@ const api = createApi({
})
```

### Updates Across Queries

The entity you want to update might be present in multiple differently shaped queries, such as
(infinite) lists, or even inside another entity. If you don't want to invalidate those instead,

Copilot AI Apr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wording is a bit unclear here: “invalidate those instead” reads like it refers to entities rather than queries. Consider rephrasing to “invalidate those queries instead” or “invalidate them instead” for clarity.

Suggested change
(infinite) lists, or even inside another entity. If you don't want to invalidate those instead,
(infinite) lists, or even inside another entity. If you don't want to invalidate those queries instead,

Copilot uses AI. Check for mistakes.
you can avoid manually looping through every single cache entry by using [selectInvalidatedBy](https://redux-toolkit.js.org/rtk-query/api/created-api/api-slice-utils#selectinvalidatedby):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
you can avoid manually looping through every single cache entry by using [selectInvalidatedBy](https://redux-toolkit.js.org/rtk-query/api/created-api/api-slice-utils#selectinvalidatedby):
you can avoid manually looping through every single cache entry by using [`selectInvalidatedBy`](https://redux-toolkit.js.org/rtk-query/api/created-api/api-slice-utils#selectinvalidatedby):

Copilot AI Apr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This link uses an absolute URL, while the rest of this page uses relative links to the local MDX docs (eg ../api/created-api/api-slice-utils.mdx#updatequerydata). Consider switching to a relative link to api-slice-utils.mdx#selectinvalidatedby so the docs work consistently in local/offline builds and across site base URLs.

Suggested change
you can avoid manually looping through every single cache entry by using [selectInvalidatedBy](https://redux-toolkit.js.org/rtk-query/api/created-api/api-slice-utils#selectinvalidatedby):
you can avoid manually looping through every single cache entry by using [selectInvalidatedBy](../api/created-api/api-slice-utils.mdx#selectinvalidatedby):

Copilot uses AI. Check for mistakes.

```ts title="Optimistic update mutation example (promise.catch, across queries)"
// file: types.ts noEmit
export interface Post {
id: number
name: string
}

// file: api.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
import type { Post } from './types'

const api = createApi({
baseQuery: fetchBaseQuery({
baseUrl: '/',
}),
tagTypes: ['Post'],
endpoints: (build) => ({
getPost: build.query<Post, number>({
query: (id) => `post/${id}`,
// highlight-start
providesTags: (result) => result ? [{ type: 'Post', id: result.id }] : [],
// highlight-end
}),
getPosts: build.infiniteQuery<Post[], void, number>({
query: ({ pageParam }) => `posts/${pageParam}`,
// highlight-start
providesTags: (result) => {
if (!result) return []

return result.pages.flatMap(page => page.map(post => ({ type: 'Post', id: post.id })))
},
// highlight-end
infiniteQueryOptions: {
initialPageParam: 1,
getNextPageParam: (lastPage, _, lastPageParam) =>
lastPage.length < 20 ? undefined : lastPageParam + 1,
},
}),
updatePost: build.mutation<void, Pick<Post, 'id'> & Partial<Post>>({
query: ({ id, ...patch }) => ({
url: `post/${id}`,
method: 'PATCH',
body: patch,
}),
// highlight-start
onQueryStarted({ id, ...patch }, { dispatch, queryFulfilled, getState }) {
const entries = api.util.selectInvalidatedBy(getState(), [{ type: 'Post', id }])

// the structure is somewhat questionable but it allows to add similar queries
// much easier, for example adding 'getUserPosts', 'getLikedPosts' would be just
// + case 'getUserPosts':
// as it's likely to also be an infinite query with the same response schema

const patches = entries.map(entry => {
switch (entry.endpointName) {
case 'getPost':
return dispatch(
api.util.updateQueryData(
entry.endpointName,
entry.originalArgs,
(draft) => {
Object.assign(draft, patch)
},
),
)
case 'getPosts':
return dispatch(
api.util.updateQueryData(
entry.endpointName,
entry.originalArgs,
(draft) => {
draft.pages.forEach(page => {
page.forEach(entity => {
if (entity.id === id) Object.assign(entity, patch)
})
})
},
),
)
default:
throw new Error(`Unknown endpoint (${entry.endpointName}) in updatePost's optimistic update`)
}
})

Comment on lines +286 to +316

Copilot AI Apr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Throwing an error here will crash the app during onQueryStarted if any other query provides the same tag but isn’t explicitly handled. For a docs example, it’s safer to ignore/skip unknown endpoints (or handle them with a no-op/log) so adding a new tagged query doesn’t introduce a runtime failure.

Suggested change
const patches = entries.map(entry => {
switch (entry.endpointName) {
case 'getPost':
return dispatch(
api.util.updateQueryData(
entry.endpointName,
entry.originalArgs,
(draft) => {
Object.assign(draft, patch)
},
),
)
case 'getPosts':
return dispatch(
api.util.updateQueryData(
entry.endpointName,
entry.originalArgs,
(draft) => {
draft.pages.forEach(page => {
page.forEach(entity => {
if (entity.id === id) Object.assign(entity, patch)
})
})
},
),
)
default:
throw new Error(`Unknown endpoint (${entry.endpointName}) in updatePost's optimistic update`)
}
})
const patches = entries.reduce((patches, entry) => {
switch (entry.endpointName) {
case 'getPost':
patches.push(
dispatch(
api.util.updateQueryData(
entry.endpointName,
entry.originalArgs,
(draft) => {
Object.assign(draft, patch)
},
),
),
)
break
case 'getPosts':
patches.push(
dispatch(
api.util.updateQueryData(
entry.endpointName,
entry.originalArgs,
(draft) => {
draft.pages.forEach(page => {
page.forEach(entity => {
if (entity.id === id) Object.assign(entity, patch)
})
})
},
),
),
)
break
default:
break
}
return patches
}, [])

Copilot uses AI. Check for mistakes.
queryFulfilled.catch(() => patches.forEach(patch => patch.undo()))

Copilot AI Apr 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the rollback handler, the callback parameter name patch shadows the outer patch object from the mutation args. Renaming the inner variable (eg patchResult/patchAction) would make the example easier to follow.

Suggested change
queryFulfilled.catch(() => patches.forEach(patch => patch.undo()))
queryFulfilled.catch(() => patches.forEach((patchResult) => patchResult.undo()))

Copilot uses AI. Check for mistakes.
},
// highlight-end
}),
}),
})
```

### General Updates

If you find yourself wanting to update cache data elsewhere in your application, you can do so
Expand Down
Loading