In RTK Query, how do providesTags and invalidatesTags refresh a posts list and a post detail after updatePost, and why add a 'LIST' id?
answer
- queries provide, mutations invalidate
- type plus optional id
- general tag hits every id
- abstract LIST id for the collection
basics
~20 sIn RTK Query, queries declare providesTags and mutations declare invalidatesTags; after a mutation, subscribed queries with a matching tag refetch. Tag each post { type: 'Post', id } and the list also with id 'LIST', so edits and additions refetch only what they affect.
solid answer
~40 sDeclare `tagTypes: ['Post']` in `createApi`. The list query provides `[...result.map(({ id }) => ({ type: 'Post', id })), { type: 'Post', id: 'LIST' }]`; the detail query provides `[{ type: 'Post', id }]`. `updatePost` invalidates `[{ type: 'Post', id }]`: that matches the detail for that id and the list, because the list also provides that specific tag — other posts' details stay cached. `addPost` invalidates `[{ type: 'Post', id: 'LIST' }]`, refetching the list but no details. A **general** tag like `['Post']` would invalidate every `Post` tag, of any id. Invalidated entries with subscribers refetch; entries with no subscribers are removed from the cache. `'LIST'` is an arbitrary id — any value that cannot collide with a real id works.
code
ts · 30 linesimport { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
type Post = { id: number; title: string; body: string };
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
tagTypes: ['Post'],
endpoints: (build) => ({
getPosts: build.query<Post[], void>({
query: () => 'posts',
providesTags: (result) =>
result
? [...result.map(({ id }) => ({ type: 'Post' as const, id })), { type: 'Post', id: 'LIST' }]
: [{ type: 'Post', id: 'LIST' }],
}),
getPost: build.query<Post, number>({
query: (id) => `posts/${id}`,
providesTags: (result, error, id) => [{ type: 'Post', id }],
}),
updatePost: build.mutation<Post, Pick<Post, 'id'> & Partial<Post>>({
query: ({ id, ...patch }) => ({ url: `posts/${id}`, method: 'PATCH', body: patch }),
// Callback form: a failed edit invalidates nothing.
invalidatesTags: (result, error, { id }) => (error ? [] : [{ type: 'Post', id }]),
}),
addPost: build.mutation<Post, Omit<Post, 'id'>>({
query: (body) => ({ url: 'posts', method: 'POST', body }),
invalidatesTags: [{ type: 'Post', id: 'LIST' }],
}),
}),
});go deeper
Know that queries provide tags, mutations invalidate them, and tag types are declared in tagTypes.
Explain general versus specific tag matching and why the list provides both per-item tags and a LIST tag.
Design tags so each mutation refetches only what it can change, and handle failed mutations with the callback form.
Set a tagging convention across many endpoints and teams so invalidation stays precise as the API grows.
## The model: provide and invalidate RTK Query's automated refetching rests on **tags**, which are labels attached to cache entries: - A **query** endpoint says which tags its cached result **provides** (`providesTags`). - A **mutation** endpoint says which tags it **invalidates** (`invalidatesTags`). - When the mutation completes, RTK Query finds every cache entry providing a matching tag. Entries with **active subscribers refetch**; entries with **no subscribers are removed** from the cache. Tag type names must be declared up front in `createApi({ tagTypes: ['Post'] })`. ## Tag shapes and how they match A tag is either a string such as `'Post'`, the same as `{ type: 'Post' }` (a **general** tag), or `{ type: 'Post', id }` (a **specific** tag). | Invalidated tag | Invalidates an entry that provided... | |---|---| | `'Post'` (general) | Any `Post` tag: general, any id, `'LIST'` | | `{ type: 'Post', id: 5 }` | A `Post` tag with id `5` | | `{ type: 'Post', id: 'LIST' }` | A `Post` tag with id `'LIST'` | A specific tag does **not** invalidate an entry that provided only the general `'Post'` tag. ## Wiring the posts list and the edit mutation 1. **List query** provides one specific tag per post plus a `LIST` tag: `providesTags: (result) => result ? [...result.map(({ id }) => ({ type: 'Post' as const, id })), { type: 'Post', id: 'LIST' }] : [{ type: 'Post', id: 'LIST' }]` 2. **Detail query** provides its own post: `providesTags: (result, error, id) => [{ type: 'Post', id }]`. 3. **`updatePost`** invalidates the edited post: `invalidatesTags: (result, error, { id }) => [{ type: 'Post', id }]`. 4. **`addPost`** invalidates the collection: `invalidatesTags: [{ type: 'Post', id: 'LIST' }]`. The outcome: - Editing post 5 refetches the detail for post 5 **and** the list (it provided `{ type: 'Post', id: 5 }`), but not post 7's detail. - Adding a post refetches the list, which is the only thing that can gain a new item, and leaves every cached detail alone. - Deleting post 5 can invalidate both `{ type: 'Post', id: 5 }` and `LIST`. ## Why the LIST id matters Without it, the list provides only per-item tags. An `addPost` mutation has no existing id to invalidate, so the list would stay stale; the only other option is the general `'Post'` tag, which would refetch **every** cached post detail too. The documentation's advice is an abstract id such as `'LIST'` that cannot collide with a real id. `'LIST'` itself is arbitrary. ## The callback form and errors Both options accept a function `(result, error, arg)`; either `result` or `error` may be `undefined`. Two consequences: - In `providesTags`, handle `result` being `undefined` when the query failed, as step 1 does. - In `invalidatesTags`, an **array** is applied whether the mutation succeeded or failed with an error returned from the base query. If a failed edit must not refetch, use the callback and return `[]` when `error` is set. ## Timing: invalidationBehavior By default (`invalidationBehavior: 'delayed'`), RTK Query holds invalidations until running queries and mutations have settled, which batches concurrent mutations and avoids refetching mid-request. `'immediately'` invalidates at once. The docs note that constant traffic can delay invalidation under the default.
- An RTK Query edit mutation fails with a 500, yet the posts list still refetches. Why?An array `invalidatesTags` is applied when the mutation settles with an error returned by the base query, not only on success. Switch to the callback form `(result, error, arg) => error ? [] : [{ type: 'Post', id: arg.id }]` so a failed edit invalidates nothing, or keep the refetch deliberately to resync after a failure.
- In RTK Query, what happens to an invalidated cache entry that no component is subscribed to?It is removed from the cache rather than refetched. Only entries with at least one active subscriber refetch. The next component that subscribes to that endpoint and argument fetches it fresh.
- Why is invalidating the general 'Post' tag after every mutation a poor default in RTK Query?A general tag matches every `Post` tag of any id, so each edit refetches the list and every cached post detail with a subscriber. It is correct but wasteful; per-item ids plus a `LIST` id refetch only what a given mutation can have changed.
saying these in an interview costs you the question
- Invalidating { type: 'Post', id: 5 } also refetches entries that provided only 'Post'
- Mutations update the cache automatically without any tags
- An array invalidatesTags is applied only when the mutation succeeds
- 'LIST' is a reserved keyword that RTK Query treats specially
- Invalidated entries always refetch, even with no subscribers