Tag Based Invalidation
What are tags
Tanstack Query encourages invalidating queries with query keys. But in our opinion query keys are hard to manage, as they are highly variable and not type safe.
For this reason, we introduce a concept of tags. Tags are a way to mark queries with a specific label. You can easily manipulate the query cache by using these tags.
This feature is heavily inspired by the same feature in RTK Query and works similarly.
How to use tags
You can start by strongly typing the data type that corresponds to each tag:
const builder = new HttpQueryBuilder().withTagTypes<{
article: ArticleData;
articles: ArticleData[];
refreshable: unknown;
}>();
We recommend using single data type for each tag. But in case a tag can be used for multiple data types, you can use a union type or unknown
type like the refreshable
tag in the example above.
Then you can use the tags in your queries:
const listArticlesQuery = builder
.withPath("/articles")
.withTag("articles", "refreshable");
The parameter passed to withTag
can be a function that returns tags. This function will be called with the query data and variables.
const singleArticleQuery = builder
.withPath("/articles/:id")
.withTag(({ vars }) => ({ type: "article", id: vars.params.id }));
Then, you can invalidate the queries in the mutations:
const deleteArticleMutation = builder
.withPath("/articles/:id")
.withMethod("delete")
// Invalidate the list of articles:
.withUpdates({ type: "articles" })
// Invalidate the single article with given id:
.withUpdates(({ vars }) => ({ type: "article", id: vars.params.id }));
Syncing between browser tabs
When invalidating queries with tags, same tags are also invalidated in other browser tabs.
This is enabled by default after calling withClient
.
You can disable this behavior by passing the option to the withClient
method.
const builder = new HttpQueryBuilder().withClient(queryClient, {
syncTagsWithOtherTabs: false,
});
Optimistic updates
You can also use the tags for updating the query cache.
The withUpdates
method accepts an updater
function for this purpose.
const deleteArticleMutation = builder
.withPath("/articles/:id")
.withMethod("delete")
.withUpdates({
type: "articles",
optimistic: true,
// Remove the article with given id from the cache:
updater: ({ vars }, cache) => cache.filter((x) => x.id !== vars.params.id),
});
When optimistic
is set to true
, the update is done optimistically.
Optimistic updates are applied immediately, before the mutation is completed.
This is useful for providing a better user experience, as the UI can be updated immediately
without waiting for the server response as opposed to the default behavior
where the UI is updated only after the server response is received.
If an error occurs during the mutation, the optimistic update is rolled back automatically.
Predefined updater functions (Experimental)
This feature is experimental and may change in the future.
You can use predefined updater functions for common operations.
const deleteArticleMutation = builder
.withPath("/articles/:id")
.withMethod("delete")
.withUpdates({
type: "articles",
updater: "delete-with-params-by-id",
});
Explanation
The predefined functions are referred as a string. This string has a format of <operation>-with-<context>-by-<field>
which can be broken down as follows:
<operation>
: The operation to be performed. This can be one ofclear, merge, replace, create, update, upsert, delete, switch
.<context>
: The body of the update that will be used in the operation. This can be one ofdata, vars, body, params, search, meta
<field>
: The field to be used in the context to match with the data in the cache. This is usually a unique identifier. This can be any field in the context.
Some examples:
delete-with-params-by-id
: remove the item from a list, whereparams.id
matches theid
field of the item.merge-with-body-by-id
: merge thebody
to the item in a list, wherebody.id
matches theid
field of the item.