Client-side caching
Apollo iOS supports client-side caching of GraphQL response data. Utilizing our caching mechanisms, your application can respond to GraphQL queries using locally cached data that has been previously fetched. This helps to reduce network traffic, which provides a number of benefits including:
Shorter loading times
Reduction of server load and cost
Less data usage for users of your application
Apollo iOS uses a normalized cache that, when configured properly, acts as a source of truth for your graph, enabling your application to react to changes as they're fetched.
The Apollo iOS library contains both a short-lived in-memory cache and a SQLite cache that persists cache data to disk.
Learn about using cache policies to configure how GraphQL operations interact with cache data by reading our documentation on fetching locally cached data.
What is a normalized cache?
In a GraphQL client, a normalized cache breaks each of your GraphQL operation responses into the individual objects it contains. Then, each object is cached as a separate entry based on its cache key. This means that if multiple responses include the same object, that object can be de-duplicated into a single cache entry. This reduces the overall size of the cache and helps keep your cached data consistent and fresh.
Because the normalized cache updates cache entries across all of your operations, data fetched by one operation can update objects fetched by another operation. This allows you to watch your queries and react to changes across your entire application. You can use this to update your UI automatically or trigger other events when new data is available.
Normalizing responses
In order to maintain a normalized cache, Apollo iOS processes response data of your GraphQL operations, identifying each object and creating new cache entries or merging data into existing cache entries.
To understand how Apollo iOS does this, consider this example query:
1query GetFavoriteBook {
2 favoriteBook { # Book object
3 id
4 title
5 author { # Author object
6 id
7 name
8 }
9 }
10}
The favoriteBook
field in this query returns a Book
object, which in turn includes an Author
object. An example response from the GraphQL server may look like this:
1{
2 "favoriteBook": {
3 "id": "bk123",
4 "title": "Les Guerriers du silence",
5 "author": {
6 "id": "au456",
7 "name": "Pierre Bordage"
8 }
9 }
10}
A normalized cache does not store this response directly. Instead, it breaks it up into individual cache entries. By default, these cache entries are identified by their path from the root operation. Because this example is a query (rather than a mutation or subscription), the root has the name QUERY_ROOT
.
1"QUERY_ROOT": {
2 "favoriteBook": "-> #QUERY_ROOT.favoriteBook"
3}
4
5"QUERY_ROOT.favoriteBook": {
6 "id": "bk123",
7 "title": "Les guerriers du silence",
8 "author": "-> #QUERY_ROOT.favoriteBook.author"
9}
10
11"QUERY_ROOT.favoriteBook.author": {
12 "id": "au456",
13 "name": "Pierre Bordage"
14}
The QUERY_ROOT
entry is always present if you've cached results from at least one query. This entry contains a reference for each top-level field you've included in any queries (e.g., favoriteBook
).
The favoriteBook
entry has a author
field containing the string "-> #QUERY_ROOT.favoriteBook.author"
. The -> #
indicates that this is a reference to another cache entry, in this case, the QUERY_ROOT.favoriteBook.author
entry.
Normalizing objects by their response path allows us to merge changes from other operations along the same response path.
For example, if we defined another query that fetched additional fields on the favoriteBook
object, they could be merged into the existing cache entry.
1query FavoriteBookYear {
2 favoriteBook { # Book object
3 id
4 yearPublished
5 }
6}
1{
2 "favoriteBook": {
3 "id": "bk123",
4 "yearPublished": 1993
5 }
6}
After merging this response into the cache, the favoriteBook
entry would have the yearPublished
field added to its existing data.
1"QUERY_ROOT.favoriteBook": {
2 "id": "bk123",
3 "title": "Les guerriers du silence",
4 "author": "-> #QUERY_ROOT.favoriteBook.author",
5 "yearPublished": 1993
6}
The favoriteBook
field can now be queried for its title
and yearPublished
in a new query, and the normalized cache could return a response from the local cache immediately without needed to send the query to the server.
1query FavoriteBookTitleAndYear {
2 favoriteBook { # Book object
3 title
4 yearPublished
5 }
6}
Normalizing objects by cache key
This section explains how cache keys are used to merge object data in the normalized cache. For information on how to configure your cache keys, see Custom cache keys.
Normalizing response data by the response path helps us de-duplicate responses for the same fields, but it does not allow us to merge cache entries from different fields that return the same object.
In this query, we fetch a Book
object using the field at the path bestFriend.favoriteBook
.
1query BestFriendsFavoriteBook {
2 bestFriend {
3 favoriteBook { # Book object
4 id
5 title
6 genre
7 }
8 }
9}
1{
2 "bestFriend" {
3 "favoriteBook": {
4 "id": "bk123",
5 "title": "Les guerriers du silence",
6 "genre": "SCIENCE_FICTION"
7 }
8 }
9}
When this response is merged into the cache, we have new cache entries added for QUERY_ROOT.bestFriend
and QUERY_ROOT.bestFriend.favoriteBook
.
The response tells use that our bestFriend
has the same favoriteBook
as us! However, the data for same book is not de-duplicated in our cache entries.
1"QUERY_ROOT.favoriteBook": {
2 "id": "bk123",
3 "title": "Les guerriers du silence",
4 "author": "-> #QUERY_ROOT.favoriteBook.author",
5 "yearPublished": 1993
6}
7
8"QUERY_ROOT.bestFriend": {
9 "favoriteBook": "-> #QUERY_ROOT.bestFriend.favoriteBook"
10}
11
12"QUERY_ROOT.bestFriend.favoriteBook": {
13 "id": "bk123",
14 "title": "Les guerriers du silence",
15 "genre": "SCIENCE_FICTION"
16}
If we tried to fetch a query with the field favoriteBook.genre
, the cache would not find the genre
field on the cache entry QUERY_ROOT.favoriteBook
, so it would send the query to the server to fetch the duplicate data.
In order to de-duplicate response data from different fields that return the same object, we need to configure the cache to recognize that they are the same object. We can do that by providing cache key configuration for the Book
object.
In this example, the Book
object type has an id
field that uniquely identifies it. Since our favoriteBook
and bestFriend.favoriteBook
cache entries have the same id
, we know they represent the same Book
object. We can configure the cache to use the id
field as the cache ID for all Book
objects. This will ensure the cache normalizes our cache entries correctly.
To configure cache keys, we return a new CacheKeyInfo
value from the SchemaConfiguration.cacheKeyInfo(for type:,object:)
function.
1static func cacheKeyInfo(for type: Object, object: JSONObject) -> CacheKeyInfo? {
2 switch type {
3 case MySchema.Objects.Book:
4 return try? CacheKeyInfo(jsonValue: object["id"])
5
6 default: return nil
7 }
8}
With this set up, whenever the normalized cache writes response data for a Book
object, it will use the id
to construct a cache key, instead of the response path.
To prevent cache key conflicts across different object types, the cache prepends the __typename
of the object to the provided cache ID followed by a colon (:
).
This means the cache key for our Book
will now be "Book:bk123"
.
For more information on using
CacheKeyInfo
to configure cache keys, see Custom cache keys.
With cache key resolution configured for the Book
type, the response data for the queries above would create a single, normalized Book
object.
1"QUERY_ROOT": {
2 "favoriteBook": "-> #Book:bk123"
3}
4
5"BOOK:bk123": {
6 "id": "bk123",
7 "title": "Les guerriers du silence",
8 "author": "-> #QUERY_ROOT.favoriteBook.author",
9 "yearPublished": 1993,
10 "genre": "SCIENCE_FICTION"
11}
12
13"QUERY_ROOT.bestFriend": {
14 "favoriteBook": "-> #Book:bk123"
15}
The cache entry for BOOK:bk123
contains all of the fields fetched on the Book
from all queries. Additionally, the favoriteBook
and bestFriend.favoriteBook
fields are a cache reference to the entry with the cache key BOOK:bk123
.
To learn more about the normalization process, see our blog posts:
Clearing cached data
All caches can be cleared in their entirety by calling clear(callbackQueue:completion:)
on your ApolloStore
.
If you need to work more directly with the cache, check out Direct cache access.