Skip to main content

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)

warning

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 of clear, merge, replace, create, update, upsert, delete, switch.
  • <context>: The body of the update that will be used in the operation. This can be one of data, 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, where params.id matches the id field of the item.
  • merge-with-body-by-id: merge the body to the item in a list, where body.id matches the id field of the item.