#
Cached DataA key feature of RTK Query is it's management of cached data. When data is fetched from the server, RTK Query will store the data in the Redux store as a 'cache'. When an additional request is performed for the same data, RTK Query will provide the existing cached data rather than sending an additional request to the server.
RTK Query provides a number of concepts and tools to manipulate the cache behaviour and adjust it to your needs.
#
Definitions#
Tagssee also: tagTypes API reference
For RTK Query, tags are just a name that you can give to a specific collection of data to control caching and invalidation behavior for refetching purposes. It can be considered as a 'label' attached to cached data that is read after a mutation
, to decide whether the data should be affected by the mutation.
Tags are defined in the tagTypes
argument when defining an api. For example, in an application that has both Posts
and Users
, you might define tagTypes: ['Post', 'User']
when calling createApi
.
An individual tag
has a type
, represented as a string
name, and an optional id
, represented as a string
or number
. It can be represented as a plain string (such as 'Post'
), or an object in the shape {type: string, id?: string|number}
(such as [{type: 'Post', id: 1}]
).
#
Providing tagssee also: providesTags API reference
A query can have it's cached data provide tags. Doing so determines which 'tag' is attached to the cached data returned by the query.
The providesTags
argument can either be an array of string
(such as ['Post']
), {type: string, id?: string|number}
(such as [{type: 'Post', id: 1}]
), or a callback that returns such an array. That function will be passed the result as the first argument, the response error as the second argument, and the argument originally passed into the query
method as the third argument. Note that either the result or error arguments may be undefined based on whether the query was successful or not.
#
Invalidating tagssee also: invalidatesTags API reference
A mutation can invalidate specific cached data based on the tags. Doing so determines which cached data will be either refetched or removed from the cache.
The invalidatesTags
argument can either be an array of string
(such as ['Post']
), {type: string, id?: string|number}
(such as [{type: 'Post', id: 1}]
), or a callback that returns such an array. That function will be passed the result as the first argument, the response error as the second argument, and the argument originally passed into the query
method as the third argument. Note that either the result or error arguments may be undefined based on whether the mutation was successful or not.
#
Default cache handling behaviourWith RTK Query, caching is based on:
- API endpoint definitions
- The serialized query parameters used when components subscribe to data from an endpoint
- Active subscription reference counts
When a subscription is started, the parameters used with the endpoint are serialized and stored internally as a queryCacheKey
for the request. Any future request that produces the same queryCacheKey
(i.e. called with the same parameters, factoring serialization) will be de-duped against the original, and will share the same data and updates. i.e. two separate components performing the same request will use the same cached data.
When a request is attempted, if the data already exists in the cache, then that data is served and no new request is sent to the server. Otherwise, if the data does not exist in the cache, then a new request is sent, and the returned response is stored in the cache.
As long as there is an active 'subscription' to the data (e.g. if a component is mounted that calls a useQuery
hook for the endpoint), then the data will remain in the cache. Once the subscription is removed (e.g. when last component subscribed to the data unmounts), after an amount of time (default 60 seconds), the data will be removed from the cache. The expiration time can be configured with the keepUnusedDataFor
property for the API definition as a whole, as well as on a per-endpoint basis.
#
Cache lifetime & subscription exampleImagine an endpoint that expects an id
as the query param, and 4 components mounted which are requesting data from this same endpoint:
While four components are subscribed to the endpoint, there are only three distinct combinations of endpoint + query parameters. Query parameters 1
and 2
will each have a single subscriber, while query parameter 3
has two subscribers. RTK Query will make three distinct fetches; one for each unique set of query parameters per endpoint.
Data is kept in the cache as long is there is at least one active subscriber interested in that endpoint + parameter combination. When the subscriber reference count reaches zero, a timer is set, and if there are no new subscriptions to that data by the time the timer expires, the cached data will be removed. The default expiration is 60 seconds, which can be configured both for the API definition as a whole, as well as on a per-endpoint basis.
If 'ComponentThree' is unmounted in the example above, regardless of how much time passes, the data will remain in the cache due to 'ComponentFour' still being subscribed to the same data, and the subscribe reference count will be one
. However, once 'ComponentFour' unmounts, the subscriber reference count will be zero
. The data will remain in the cache for the remainder of the expiration time, at which point, if no new subscription has been created during that time, the cached data will finally be removed.
#
Cache tagsRTK Query uses the concept of 'tags' to determine whether a mutation for one endpoint intends to 'invalidate' some data that was 'provided' by a query from another endpoint.
If cache data is being invalidated, it will either refetch the providing query (if components are still using that data) or remove the data from the cache.
When defining an api
, createApi
accepts an array of tag type names for the tagTypes
property, which is a list of possible tag name options that the queries for the api
could provide.
The example below declares that endpoints can possibly provide 'Posts' and/or 'Users' to the cache:
- TypeScript
- JavaScript
By declaring these tags as what can possibly be provided to the cache, it enables control for individual mutation endpoints to claim whether they affect specific portions of the cache or not, in conjunction with providesTags
and invalidatesTags
on individual endpoints.
#
Providing cache dataEach individual query
endpoint can have it's cached data provide
particular tags. Doing so enables a relationship between cached data from one or more query
endpoints and the behaviour of one or more mutation
endpoints.
The providesTags
property on a query
endpoint is used for this purpose.
Note
Provided tags have no inherent relationship across separate
query
endpoints. Provided tags are used to determine whether cached data returned by an endpoint should beinvalidated
and either be refetched or removed from the cache. If two separate endpoints provide the same tags, they will still contribute their own distinct cached data, which could later both be invalidated by a single tag declared from a mutation.
The example below declares that the getPosts
query
endpoint provides
the 'Post'
tag to the cache, using the providesTags
property for a query
endpoint.
- TypeScript
- JavaScript
For more granular control over the provided data, provided tags
can have an associated id
. This enables a distinction between 'any of a particular tag type', and 'a specific instance of a particular tag type'.
The example below declares that the provided posts are associated with particular IDs as determined by the result returned by the endpoint:
- TypeScript
- JavaScript
Note that for the example above, the id
is used where possible on a successful result. In the case of an error, no result is supplied, and we still consider that it has provided the general 'Post'
tag type rather than any specific instance of that tag.
Advanced List Invalidation
In order to provide stronger control over invalidating the appropriate data, you can use an arbitrary ID such a 'LIST'
for a given tag. See Advanced Invalidation with abstract tag IDs for additional details.
#
Invalidating cache dataEach individual mutation endpoint can invalidate
particular tags for existing cached data. Doing so enables a relationship between cached data from one or more query endpoints and the behaviour of one or more mutation endpoints.
The invalidatesTags
property on a mutation endpoint is used for this purpose.
The example below declares that the addPost
and editPost
mutation endpoints invalidate
any cached data with the 'Post'
tag, using the invalidatesTags
property for a mutation endpoint:
- TypeScript
- JavaScript
For the example above, this tells RTK Query that after the addPost
and/or editPost
mutations are called and completed, any cache data supplied with the 'Post'
tag is no longer valid. If a component is currently subscribed to the cached data for a 'Post'
tag after the above mutations are called and complete, it will automatically re-fetch in order to retrieve up to date data from the server.
An example scenario would be like so:
- A component is rendered which is using the
useGetPostsQuery()
hook to subscribe to that endpoint's cached data - The
/posts
request is fired off, and server responds with posts with IDs 1, 2 & 3 - The
getPosts
endpoint stores the received data in the cache, and internally registers that the following tags have been provided: - The
editPost
mutation is fired off to alter a particular post - Upon completion, RTK Query internally registers that the
'Post'
tag is now invalidated, and removes the previously provided'Post'
tags from the cache - Since the
getPosts
endpoint has provided tags of type'Post'
which now has invalid cache data, and the component is still subscribed to the data, the/posts
request is automatically fired off again, fetching new data and registering new tags for the updated cached data
For more granular control over the invalidated data, invalidated tags
can have an associated id
in the same manner as providesTags
. This enables a distinction between 'any of a particular tag type' and 'a specific instance of a particular tag type'.
The example below declares that the editPost
mutation invalidates a specific instance of a Post
tag, using the ID passed in when calling the mutation function:
- TypeScript
- JavaScript
For the example above, rather than invalidating any tag with the type 'Post'
, calling the editPost
mutation function will now only invalidate a tag for the provided id
. I.e. if cached data from an endpoint does not provide a 'Post'
for that same id
, it will remain considered as 'valid', and will not be triggered to automatically re-fetch.
Using abstract tag IDs
In order to provide stronger control over invalidating the appropriate data, you can use an arbitrary ID such a 'LIST'
for a given tag. See Advanced Invalidation with abstract tag IDs for additional details.
#
Invalidation BehaviorThe matrix below shows examples of which invalidated tags will affect and invalidate which provided tags:
Provided Invalidated | General tag A ['Post'] / [{ type: 'Post' }] | General tag B ['User'] / [{ type: 'User' }] | Specific tag A1 [{ type: 'Post', id: 1 }] | Specific tag A2 [{ type: 'Post', id: 'LIST' }] | Specific tag B1 [{ type: 'User', id: 1 }] | Specific tag B2 [{ type: 'User', id: 2 }] |
---|---|---|---|---|---|---|
General tag A ['Post'] / [{ type: 'Post' }] | โ๏ธ | โ๏ธ | โ๏ธ | |||
General tag B ['User'] / [{ type: 'User' }] | โ๏ธ | โ๏ธ | โ๏ธ | |||
Specific tag A1 [{ type: 'Post', id: 1 }] | โ๏ธ | |||||
Specific tag A2 [{ type: 'Post', id: 'LIST' }] | โ๏ธ | |||||
Specific tag B1 [{ type: 'User', id: 1 }] | โ๏ธ | |||||
Specific tag B2 [{ type: 'User', id: 2 }] | โ๏ธ |
The invalidation behavior can be summarized like so:
General tag
e.g. ['Post'] / [{ type: 'Post' }]
Will invalidate
any provided
tag with the matching type, including general and specific tags.
Example:
If a general tag of Post
was invalidated, endpoints whose data provided
the following tags would all have their data invalidated:
['Post']
[{ type: 'Post' }]
[{ type: 'Post' }, { type: 'Post', id: 1 }]
[{ type: 'Post', id: 1 }]
[{ type: 'Post', id: 1 }, { type: 'User' }]
[{ type: 'Post', id: 'LIST' }]
[{ type: 'Post', id: 1 }, { type: 'Post', id: 'LIST' }]
Endpoints whose data provided
the following tags would not have their data invalidated:
['User']
[{ type: 'User' }]
[{ type: 'User', id: 1 }]
[{ type: 'User', id: 'LIST' }]
[{ type: 'User', id: 1 }, { type: 'User', id: 'LIST' }]
Specific tag
e.g. [{ type: 'Post', id: 1 }]
Will invalidate
any provided
tag with both the matching type, and matching id. Will not cause a general
tag to be invalidated directly, but might invalidate data for an endpoint that provides a general
tag if it also provides a matching specific
tag.
Example 1:
If a specific tag of { type: 'Post', id: 1 }
was invalidated, endpoints whose data provided
the following tags would all have their data invalidated:
[{ type: 'Post' }, { type: 'Post', id: 1 }]
[{ type: 'Post', id: 1 }]
[{ type: 'Post', id: 1 }, { type: 'User' }]
[{ type: 'Post', id: 1 }, { type: 'Post', id: 'LIST' }]
Endpoints whose data provided
the following tags would not have their data invalidated:
['Post']
[{ type: 'Post' }]
[{ type: 'Post', id: 'LIST' }]
['User']
[{ type: 'User' }]
[{ type: 'User', id: 1 }]
[{ type: 'User', id: 'LIST' }]
[{ type: 'User', id: 1 }, { type: 'User', id: 'LIST' }]
Example 2:
If a specific tag of { type: 'Post', id: 'LIST' }
was invalidated, endpoints whose data provided
the following tags would all have their data invalidated:
[{ type: 'Post', id: 'LIST' }]
[{ type: 'Post', id: 1 }, { type: 'Post', id: 'LIST' }]
Endpoints whose data provided
the following tags would not have their data invalidated:
['Post']
[{ type: 'Post' }]
[{ type: 'Post' }, { type: 'Post', id: 1 }]
[{ type: 'Post', id: 1 }]
[{ type: 'Post', id: 1 }, { type: 'User' }]
['User']
[{ type: 'User' }]
[{ type: 'User', id: 1 }]
[{ type: 'User', id: 'LIST' }]
[{ type: 'User', id: 1 }, { type: 'User', id: 'LIST' }]
#
Recipes#
Advanced Invalidation with abstract tag IDsWhile using an 'entity ID' for a tag id
is a common use case, the id
property is not intended to be limited to database IDs alone. The id
is simply a way to label a subset of a particular collection of data for a particular tag type
.
A powerful use-case is to use an ID like 'LIST'
as a label for data provided by a bulk query, as well as using entity IDs for the individual items. Doing so allows future mutations
to declare whether they invalidate the data only if it contains a particular item (e.g. { type: 'Post', id: 5 }
), or invalidate the data if it is a 'LIST'
(e.g. { type: 'Post', id: 'LIST' }
).
Note about 'LIST' and
id
s
LIST
is an arbitrary string - technically speaking, you could use anything you want here, such asALL
or*
. The important thing when choosing a custom id is to make sure there is no possibility of it colliding with an id that is returned by a query result. If you have unknown ids in your query results and don't want to risk it, you can go with point 3 below.- You can add many tag types for even more control
[{ type: 'Posts', id: 'LIST' }, { type: 'Posts', id: 'SVELTE_POSTS' }, { type: 'Posts', id: 'REACT_POSTS' }]
- If the concept of using an
id
like 'LIST' seems strange to you, you can always add anothertagType
and invalidate it's root, but we recommend using theid
approach as shown.
We can compare the scenarios below to see how using a 'LIST'
id can be leveraged to optimize behaviour.
#
Invalidating everything of a type- TypeScript
- JavaScript
What to expect
When addPost
is triggered, it would cause each PostDetail
component to go back into a isFetching
state because addPost
invalidates the root tag, which causes every query that provides 'Posts' to be re-run. In most cases, this may not be what you want to do. Imagine if you had 100 posts on the screen that all subscribed to a getPost
query โ in this case, you'd create 100 requests and send a ton of unnecessary traffic to your server, which we're trying to avoid in the first place! Even though the user would still see the last good cached result and potentially not notice anything other than their browser hiccuping, you still want to avoid this.
#
Selectively invalidating lists- TypeScript
- JavaScript
What to expect
When addPost
is fired, it will only cause the PostsList
to go into an isFetching
state because addPost
only invalidates the 'LIST'
id, which causes getPosts
to rerun (because it provides that specific id). So in your network tab, you would only see 1 new request fire for GET /posts
. As the singular getPost
queries have not been invalidated, they will not re-run as a result of addPost
.
Note
If you intend for the
addPost
mutation to refresh all posts including individualPostDetail
components while still only making 1 newGET /posts
request, this can be done by selecting a part of the data usingselectFromResult
.
#
Providing errors to the cacheThe information provided to the cache is not limited to successful data fetches. The concept can be used to inform RTK Query that when a particular failure has been encountered, to provide
a specific tag
for that failed cache data. A separate endpoint can then invalidate
the data for that tag
, telling RTK Query to re-attempt the previously failed endpoints if a component is still subscribed to the failed data.
The example below demonstrates an example with the following behaviour:
- Provides an
UNAUTHORIZED
cache tag if a query fails with an error code of401 UNAUTHORIZED
- Provides an
UNKNOWN_ERROR
cache tag if a query fails with a different error - Enables a 'login' mutation, which when successful, will
invalidate
the data with theUNAUTHORIZED
tag.
This will trigger thepostById
endpoint to re-fire if:- The last call for
postById
had encountered an unauthorized error, and - A component is still subscribed to the cached data
- The last call for
- Enables a 'refetchErroredQueries' mutation which when called, will
invalidate
the data with theUNKNOWN_ERROR
tag.
This will trigger thepostById
endpoint to re-fire if:- The last call for
postById
had encountered an unknown error, and - A component is still subscribed to the cached data
- The last call for
- TypeScript
- JavaScript
#
Abstracting common provides/invalidates usageThe code written to provide
& invalidate
tags for a given api
will be dependent on multiple factors, including:
- The shape of the data returned by your backend
- Which tags you expect a given query endpoint to provide
- Which tags you expect a given mutation endpoint to invalidate
- The extent that you wish to use the invalidation feature for
When declaring your api
, you may feel as though you're duplicating your code. For instance, for two separate endpoints that both provide a list of a particular entity, the providesTags
declaration may only differ in the tagType
provided.
e.g.
- TypeScript
- JavaScript
You may find it beneficial to define helper functions designed for your particular api to reduce this boilerplate across endpoint definitions, e.g.
- TypeScript
- JavaScript
An example of various abstractions for tag providing/invalidating designed for common rest data formats can be seen in the following gist, including typescript support, and factoring both 'LIST' style advanced tag invalidation and 'error' style tag invalidation: RTK Query cache utils.