Caching in Apollo Client

Overview


Apollo Client stores the results of your GraphQL queries in a local, normalized, in-memory cache. This enables Apollo Client to respond almost immediately to queries for already-cached data, without even sending a network request.

For example, the first time your app executes a GetBook query for a Book object with id 5, the flow looks like this:

And each later time your app executes GetBook for that same object, the flow looks like this instead:

The Apollo Client cache is highly configurable. You can customize its behavior for individual types and fields in your schema, and you can even use it to store and interact with local data that isn't fetched from your GraphQL server.

How is data stored?

Apollo Client's InMemoryCache stores data as a flat lookup table of objects that can reference each other. These objects correspond to the objects that are returned by your GraphQL queries. A single cached object might include fields returned by multiple queries, if those queries fetch different fields of the same object.

The cache is flat, but objects returned by a GraphQL query often aren't! In fact, their nesting can be arbitrarily deep. Take a look at this example query response:

JSON
1{
2  "data": {
3    "person": {
4      "__typename": "Person",
5      "id": "cGVvcGxlOjE=",
6      "name": "Luke Skywalker",
7      "homeworld": {
8        "__typename": "Planet",
9        "id": "cGxhbmV0czox",
10        "name": "Tatooine"
11      }
12    }
13  }
14}

This response contains a Person object, which in turn contains a Planet object in its homeworld field.

So how does the InMemoryCache store nested data in a flat lookup table? Before storing this data, the cache needs to normalize it.

Data normalization

Whenever the Apollo Client cache receives query response data, it does the following:

1. Identify objects

First, the cache identifies all of the distinct objects included in a query response. In the example above, there are two objects:

  • A Person with id cGVvcGxlOjE=

  • A Planet with id cGxhbmV0czox

2. Generate cache IDs

After identifying all objects, the cache generates a cache ID for each one. A cache ID uniquely identifies a particular object while it's in the InMemoryCache.

By default, an object's cache ID is the concatenation of the object's __typename and id (or _id) fields, separated by a colon (:).

So, the default cache IDs for the objects in the example above are:

  • Person:cGVvcGxlOjE=

  • Planet:cGxhbmV0czox

You can customize the cache ID format for a particular object type. See Customizing cache IDs.

If the cache can't generate a cache ID for a particular object (for example, if no id or _id field is present), that object is cached directly inside its parent object, and it must be referenced via the parent (this means the cache isn't always completely flat).

3. Replace object fields with references

Next, the cache takes each field that contains an object and replaces its value with a reference to the appropriate object.

For example, here's the Person object from the example above before reference replacement:

JSON
1{
2  "__typename": "Person",
3  "id": "cGVvcGxlOjE=",
4  "name": "Luke Skywalker",
5  "homeworld": {
6    "__typename": "Planet",
7    "id": "cGxhbmV0czox",
8    "name": "Tatooine"
9  }
10}

And here's that same object after replacement:

JSON
1{
2  "__typename": "Person",
3  "id": "cGVvcGxlOjE=",
4  "name": "Luke Skywalker",
5  "homeworld": {
6    "__ref": "Planet:cGxhbmV0czox"
7  }
8}

The homeworld field now contains a reference to the appropriate normalized Planet object.

This replacement does not occur for a particular object if the previous step failed to generate a cache ID for that object. Instead, the original object remains.

Later, if you query for another Person who has the same homeworld, that normalized Person object will contain a reference to the same cached object! Normalization can dramatically reduce data duplication, and it also helps your local data stay up to date with your server.

4. Store normalized objects

Finally, the resulting objects are all stored in the cache's flat lookup table.

Whenever an incoming object has the same cache ID as an existing cached object, the fields of those objects are merged:

  • If the incoming object and the existing object share any fields, the incoming object overwrites the cached values for those fields.

  • Fields that appear in only the existing object or only the incoming object are preserved.

Normalization constructs a partial copy of your graph on your client, in a format that's optimized for reading and updating as your app's state changes.

Visualizing the cache

To help understand the structure of your cached data, we strongly recommend installing the Apollo Client Devtools.

This browser extension includes an inspector that enables you to view all of the normalized objects contained in your cache:

The Cache tab of the Apollo Client Devtools

Example

Let's say we use Apollo Client to run the following query on the SWAPI demo API:

GraphQL
1query {
2  allPeople(first:3) { # Return the first 3 items
3    people {
4      id
5      name
6      homeworld {
7        id
8        name
9      }
10    }
11  }
12}

This query returns the following result of three Person objects, each with a corresponding homeworld (a Planet object):

Click to expand
JSON
1{
2  "data": {
3    "allPeople": {
4      "people": [
5        {
6          "__typename": "Person",
7          "id": "cGVvcGxlOjE=",
8          "name": "Luke Skywalker",
9          "homeworld": {
10            "__typename": "Planet",
11            "id": "cGxhbmV0czox",
12            "name": "Tatooine"
13          }
14        },
15        {
16          "__typename": "Person",
17          "id": "cGVvcGxlOjI=",
18          "name": "C-3PO",
19          "homeworld": {
20            "__typename": "Planet",
21            "id": "cGxhbmV0czox",
22            "name": "Tatooine"
23          }
24        },
25        {
26          "__typename": "Person",
27          "id": "cGVvcGxlOjM=",
28          "name": "R2-D2",
29          "homeworld": {
30            "__typename": "Planet",
31            "id": "cGxhbmV0czo4",
32            "name": "Naboo"
33          }
34        }
35      ]
36    }
37  }
38}

Notice that each object in the result includes a __typename field, even though our query string didn't include this field. That's because Apollo Client automatically queries for every object's __typename.

After the result is cached, we can view the state of our cache in the Apollo Client Devtools:

The Cache tab of the Apollo Client Devtools

Our cache now contains five normalized objects (in addition to the ROOT_QUERY object): three Person objects and two Planet objects.

Why do we only have two Planet objects? Because two of the three returned Person objects have the same homeworld. By normalizing data like this, Apollo Client can cache a single copy of an object, and multiple other objects can include references to it (see the __ref field of the object in the screenshot above).

Next steps

Now that you have a basic understanding of how Apollo Client's cache works, learn more about how to configure it.

Then, you can learn how to read and write data to your cache directly, without executing a query against your server. This is a powerful option for local state management.

Feedback

Edit on GitHub

Forums