Launch Apollo Studio

Advanced topics on caching in Apollo Client


This article describes special cases and considerations when using the Apollo Client cache.

Bypassing the cache

Sometimes you shouldn't use the cache for a particular GraphQL operation. For example, a query's response might be a token that's only used once. In cases like this, use the no-cache fetch policy:

const { loading, error, data } = useQuery(GET_DOGS, {
  fetchPolicy: "no-cache"});

Operations that use this fetch policy don't write their result to the cache, and they also don't check the cache for data before sending a request to your server. See all available fetch policies.

Persisting the cache

You can persist and rehydrate the InMemoryCache from a storage provider like AsyncStorage or localStorage. To do so, use the apollo3-cache-persist library. This library works with a variety of storage providers.

To get started, pass your cache and a storage provider to persistCache. By default, the contents of your cache are immediately restored asynchronously, and they're persisted on every write to the cache with a short configurable debounce interval.

Note: The persistCache method is async and returns a Promise.

import { AsyncStorage } from 'react-native';
import { InMemoryCache } from '@apollo/client';
import { persistCache } from 'apollo3-cache-persist';

const cache = new InMemoryCache();

persistCache({
  cache,
  storage: AsyncStorage,
}).then(() => {
  // Continue setting up Apollo Client as usual.
})

For advanced usage and additional configuration options, see the README of apollo3-cache-persist.

Resetting the cache

Sometimes, you might want to reset the cache entirely, such as when a user logs out. To accomplish this, call client.resetStore. This method is asynchronous, because it also refetches any of your active queries.

export default withApollo(graphql(PROFILE_QUERY, {
  props: ({ data: { loading, currentUser }, ownProps: { client }}) => ({
    loading,
    currentUser,
    resetOnLogout: async () => client.resetStore(),
  }),
})(Profile));

To reset the cache without refetching active queries, use client.clearStore() instead of client.resetStore().

Responding to cache resets

You can register callback functions that execute whenever client.resetStore is called. To do so, call client.onResetStore and pass in your callback. To register multiple callbacks, call client.onResetStore multiple times. All of your callbacks are added to an array and are executed concurrently whenever the cache is reset.

In this example, we use 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.

import { ApolloClient, InMemoryCache } from '@apollo/client';
import { withClientState } from 'apollo-link-state';

import { resolvers, defaults } from './resolvers';

const cache = new InMemoryCache();
const stateLink = withClientState({ cache, resolvers, defaults });

const client = new ApolloClient({
  cache,
  link: stateLink,
});

client.onResetStore(stateLink.writeDefaults);

You can also call client.onResetStore from your React components. This can be useful if you want to force your UI to rerender after the cache is reset.

The client.onResetStore method's return value is a function you can call to unregister your callback:

import { withApollo } from "@apollo/react-hoc";

export class Foo extends Component {
  constructor(props) {
    super(props);
    this.unsubscribe = props.client.onResetStore(      () => this.setState({ reset: false })    );    this.state = { reset: false };
  }
  componentDidUnmount() {
    this.unsubscribe();  }
  render() {
    return this.state.reset ? <div /> : <span />
  }
}

export default withApollo(Foo);

Refetching queries after a mutation

In certain cases, writing an update function to update the cache after a mutation can be complex, or even impossible if the mutation doesn't return modified fields.

In these cases, you can provide a refetchQueries option to the useMutation hook to automatically rerun certain queries after the mutation completes.

For details, see Refetching queries.

Note that although refetchQueries can be faster to implement than an update function, it also requires additional network requests that are usually undesirable. For more information, see this blog post.

Cache redirects

In some cases, a query requests data that already exists in the cache under a different reference. For example, your UI might have 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 might display an individual item using this query:

query Book($id: ID!) {
  book(id: $id) {
    id
    title
    abstract
  }
}

In a case like this, we know that the second query's data might already be in the cache, but because that data was fetched by a different query, Apollo Client doesn't know that. To tell Apollo Client where to look for the cached Book object, we can define a field policy read function for the book field:

import { ApolloClient, InMemoryCache } from '@apollo/client';

const client = new ApolloClient({
  cache: new InMemoryCache({
    typePolicies: {
      Query: {
        fields: {
          book: {
            read(_, { args, toReference }) {              return toReference({                __typename: 'Book',                id: args.id,              });            }          }
        }
      }
    }
  }
});

This read function uses the toReference helper utility to generate and return a cache reference for a Book object, based on its __typename and id.

Now whenever a query includes the book field, the read function above executes and returns a reference to a Book object. Apollo Client uses this reference to look up the object in its cache and return it if it's present. If it isn't present, Apollo Client knows it needs to execute the query over the network.

⚠️ Note: To avoid a network request, all of a query's requested fields must already be present in the cache. If the detail view's query fetches any Book field that the list view's query didn't, Apollo Client considers the cache hit to be incomplete, and it executes the full query over the network.

Pagination utilities

Incremental loading: fetchMore

You can use the fetchMore function to update a query's cached result with data returned by a followup query. Most often, fetchMore is used to handle infinite-scroll pagination and other situations where you're loading more data when you already have some.

For details, see The fetchMore function.

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 fetchMores, 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.

client.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.

Edit on GitHub