Advanced Topics on Caching
Here are some common situations where you would need to access the cache directly. If you’re manipulating the cache in an interesting way and would like your example to be featured, please send in a pull request!
Bypassing the cache
Sometimes it makes sense to not use the cache for a specific operation. This can be done using the
no-cache
fetchPolicy
. The no-cache
policy does not write to the cache with the response. This
may be useful for sensitive data like passwords that you don’t want to keep in the cache.
Updating after a mutation
In some cases, just using dataIdFromObject
is not enough for your application UI to update
correctly. For example, if you want to add something to a list of objects without refetching the
entire list, or if there are some objects that to which you can’t assign an object identifier,
Apollo Client cannot update existing queries for you. Read on to learn about the other tools at your
disposal.
refetchQueries
is the simplest way of updating the cache. With refetchQueries
you can specify
one or more queries that you want to run after a mutation is completed in order to refetch the parts
of the store that may have been affected by the mutation:
apollo.mutate({
// ... insert comment mutation
refetchQueries: [
{
query: gql`
query UpdateCache($repoName: String!) {
entry(repoFullName: $repoName) {
id
comments {
postedBy {
login
html_url
}
createdAt
content
}
}
}
`,
variables: { repoName: 'apollographql/apollo-client' },
},
],
});
Please note that if you call refetchQueries
with an array of strings, then Apollo Client will look
for any previously called queries that have the same names as the provided strings. It will then
refetch those queries with their current variables.
A very common way of using refetchQueries
is to import queries defined for other components to
make sure that those components will be updated:
import RepoCommentsQuery from '../queries/RepoCommentsQuery';
apollo.mutate({
//... insert comment mutation
refetchQueries: [
{
query: RepoCommentsQuery,
variables: { repoFullName: 'apollographql/apollo-client' },
},
],
});
Using update
gives you full control over the cache, allowing you to make changes to your data
model in response to a mutation in any way you like. update
is the recommended way of updating the
cache after a query. It is explained in full here.
import CommentAppQuery from '../queries/CommentAppQuery';
const SUBMIT_COMMENT_MUTATION = gql`
mutation SubmitComment($repoFullName: String!, $commentContent: String!) {
submitComment(
repoFullName: $repoFullName
commentContent: $commentContent
) {
postedBy {
login
html_url
}
createdAt
content
}
}
`;
@Component({
// ...
})
class CommentsPageComponent {
constructor(private readonly apollo: Apollo) {}
submit({repoFullName, commentContent}) {
this.apollo.mutate({
mutation: SUBMIT_COMMENT_MUTATION,
variables: { repoFullName, commentContent },
update: (store, { data: { submitComment } }) => {
// Read the data from our cache for this query.
const data = store.readQuery({ query: CommentAppQuery });
// Add our comment from the mutation to the end.
const comments = [...data.comments, submitComment];
// Write our data back to the cache.
store.writeQuery({ query: CommentAppQuery, { comments } });
}
})
}
}
Incremental loading: fetchMore
fetchMore
can be used to update the result of a query based on the data returned by another query.
Most often, it is used to handle infinite-scroll pagination or other situations where you are
loading more data when you already have some.
In our GitHunt example, we have a paginated feed that displays a list of GitHub repositories. When we hit the “Load More” button, we don’t want Apollo Client to throw away the repository information it has already loaded. Instead, it should just append the newly loaded repositories to the list that Apollo Client already has in the store. With this update, our UI component should re-render and show us all of the available repositories.
Let’s see how to do that with the fetchMore
method on a query:
const FEED_QUERY = gql`
query Feed($offset: Int, $limit: Int) {
feed(offset: $offset, limit: $limit) {
id
# ...
}
}
`;
@Component({
// ...
})
class FeedComponent {
feedQuery: QueryRef<any>;
feed: Observable<any>;
offset: number;
constructor(private apollo: Apollo) {
this.feedQuery = this.apollo.watchQuery({
query: FEED_QUERY,
variables: {
offset: 0,
limit: 10,
},
fetchPolicy: 'cache-and-network',
});
this.feed = this.feedQuery.valueChanges.pipe(
tap(result => {
this.offset = result.data.feed.length;
}),
map(result => result.data.feed),
);
}
loadMore() {
this.feedQuery.fetchMore({
variables: {
offset: this.offset,
},
updateQuery: (prev, { fetchMoreResult }) => {
if (!fetchMoreResult) return prev;
return Object.assign({}, prev, {
feed: [...prev.feed, ...fetchMoreResult.feed],
});
},
});
}
}
The fetchMore
method takes a map of variables
to be sent with the new query. Here, we’re setting
the offset to feed.length
so that we fetch items that aren’t already displayed on the feed. This
variable map is merged with the one that’s been specified for the query associated with the
component. This means that other variables, e.g. the limit
variable, will have the same value as
they do within the component query.
It can also take a query
named argument, which can be a GraphQL document containing a query that
will be fetched in order to fetch more information; we refer to this as the fetchMore
query. By
default, the fetchMore
query is the query associated with the container, in this case the
FEED_QUERY
.
When we call fetchMore
, Apollo Client will fire the fetchMore
query and use the logic in the
updateQuery
option to incorporate that into the original result. The named argument updateQuery
should be a function that takes the previous result of the query associated with your component
(i.e. FEED_QUERY
in this case) and the information returned by the fetchMore
query and return a
combination of the two.
Here, the fetchMore
query is the same as the query associated with the component. Our
updateQuery
takes the new feed items returned and just appends them onto the feed items that we’d
asked for previously. With this, the UI will update and the feed will contain the next page of
items!
Although fetchMore
is often used for pagination, there are many other cases in which it is
applicable. For example, suppose you have a list of items (say, a collaborative todo list) and you
have a way to fetch items that have been updated after a certain time. Then, you don’t have to
refetch the whole todo list to get updates: you can just incorporate the newly added items with
fetchMore
, as long as your updateQuery
function correctly merges the new results.
The @connection
directive
Fundamentally, paginated queries are the same as any other query with the exception that calls to
fetchMore
update the same cache key. Since these queries are cached by both the initial query and
their parameters, a problem arises when later retrieving or updating paginated queries in the cache.
We don’t care about pagination arguments such as limits, offsets, or cursors outside of the need to
fetchMore
, nor do we want to provide them simply for accessing cached data.
To solve this Apollo Client 1.6 introduced the @connection
directive to specify a custom store key
for results. A connection allows us to set the cache key for a field and to filter which arguments
actually alter the query.
To use the @connection
directive, simply add the directive to the segment of the query you want a
custom store key for and provide the key
parameter to specify the store key. In addition to the
key
parameter, you can also include the optional filter
parameter, which takes an array of query
argument names to include in the generated custom store key.
const query = gql`
query Feed($type: FeedType!, $offset: Int, $limit: Int) {
feed(type: $type, offset: $offset, limit: $limit) @connection(key: "feed", filter: ["type"]) {
...FeedEntry
}
}
`;
With the above query, even with multiple fetchMore
s, the results of each feed update will always
result in the feed
key in the store being updated with the latest accumulated values. In this
example, we also use the @connection
directive’s optional filter
argument to include the type
query argument in the store key, which results in multiple store values that accumulate queries from
each type of feed.
Now that we have a stable store key, we can easily use writeQuery
to perform a store update, in
this case clearing out the feed.
cache.writeQuery({
query: gql`
query Feed($type: FeedType!) {
feed(type: $type) @connection(key: "feed", filter: ["type"]) {
id
}
}
`,
variables: {
type: 'top',
},
data: {
feed: [],
},
});
Note that because we are only using the type
argument in the store key, we don’t have to provide
offset
or limit
.
Cache redirects using field policy read
functions
Note: Apollo Client >= 3.0 no longer supports the ApolloClient
’s cacheRedirects
constructor option. Equivalent cacheRedirects
functionality can now be handled with field policy
read
functions, and is explained below.
In some cases, a query requests data that already exists in the cache under a different reference. A very common example of this is when your UI has a list view and a detail view that both use the same data. The list view might run the following query:
query Books {
books {
id
title
abstract
}
}
When a specific book is selected, the detail view displays an individual item using this query:
query Book($id: ID!) {
book(id: $id) {
id
title
abstract
}
}
We know that the data is most likely already in the client cache, but because it was requested with
a different query, Apollo Client doesn’t know that. To tell Apollo Client where to look for the
existing book
data, we can define a field policy read
function for the book
field:
import { InMemoryCache } from '@apollo/client/core';
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
book(_, { args, toReference }) {
return toReference({
__typename: 'Book',
id: args.id,
});
},
},
},
},
});
Now whenever a query is run that includes a book
field, the read
function above will be
executed, and return a reference that points to the book entity that was already created in the
cache when the Books
list view query ran. Apollo Client will use the reference returned by the
read
function to look up the item in its cache. toReference
is a helper utility that is passed
into read
functions as part of the second parameter options object, and is used to generate an
entity reference based on its __typename
and id
.
Note: For the above to work properly, the data returned by the list query has to include all of the data the specific detail query needs. If the specific detail query fetches a field that the list query doesn’t return, Apollo Client will consider the cache hit to be incomplete, and will attempt to fetch the full data set over the network (if network requests are enabled).
Resetting the store
Sometimes, you may want to reset the store entirely, such as
when a user logs out. To accomplish this, use
client.resetStore
to clear out your Apollo cache. Since client.resetStore
also refetches any of
your active queries for you, it is asynchronous.
To register a callback function to be executed after the store has been reset, call
client.onResetStore
and pass in your callback. If you would like to register multiple callbacks,
simply call client.onResetStore
again. All of your callbacks will be pushed into an array and
executed concurrently.
In this example, we’re using client.onResetStore
to write default values to the cache. This is
useful when using Apollo Client’s local state management features and
calling client.resetStore
anywhere in your application.
You can also call client.onResetStore
from your React components. This can be useful if you would
like to force your UI to rerender after the store has been reset.
If you want to clear the store but don’t want to refetch active queries, use client.clearStore()
instead of client.resetStore()
.
Cache persistence
If you would like to persist and rehydrate your Apollo Cache from a storage provider like
localStorage
, you can use
apollo3-cache-persist
.
apollo3-cache-persist
works with all Apollo caches, including InMemoryCache
& Hermes
, and a
variety of different
storage providers.
To get started, simply pass your Apollo Cache and a storage provider to persistCacheSync
. By
default, the contents of your Apollo Cache will be immediately restored, and persisted upon every
write to the cache with a short configurable debounce interval.
import { LocalStorageWrapper, persistCacheSync } from 'apollo3-cache-persist';
import { InMemoryCache } from '@apollo/client/core';
const cache = new InMemoryCache();
persistCacheSync({
cache,
storage: new LocalStorageWrapper(window.LocalStorage),
});
For more advanced usage, such as persisting the cache when the app is in the background, and
additional configuration options, please check the
README of apollo3-cache-persist
.