Caching is a common computing technique that stores copies of data in a cache, or temporary storage, so that when the same data is next requested it can be delivered much faster than retrieving the data from the original location it was stored.
To explain with an analogy, imagine working at your desk and wanting a cup of tea. The process for making yourself tea is to wearily trudge to your kitchen, perform the process for making tea, then return to your desk with your cup. But wanting to save yourself time you invest in a teapot! You fill it up once then you can quickly refill your tea cup without having to make another long and time-consuming trip to the kitchen. Now you have more time to invest in fixing that bug you just can’t seem to wrap your head around!
In the digital world caching improves data retrieval performance and makes web applications feel a lot more responsive. Here at Administrate, our API is built using GraphQL and most of our front-end services utilise Apollo Client to fetch data from our API. Apollo Client features comprehensive caching functionality allowing you to query data without having to send a network request.
Configuring the cache
The first step in utilising Apollo Client’s cache functionality is when the client is configured. To set up the cache, an instance of InMemoryCache()
must be passed as a parameter when creating the client.
const client = new ApolloClient({
uri: "https://mygraphqlserver.com",
cache: new InMemoryCache(),
});
If using Apollo Client 3.0 you can import
InMemoryCache()
from the"@apollo/client"
package. However, if you are using 2.0 you must install theapollo-cache-inmemory
package and import it from there.
Data normalization in the cache
It’s useful to understand how the Apollo Client cache normalizes and stores its data. It does this in the following steps:
-
When the client receives query response data it will first identify all the distinct objects. Here is an example of the response we may get from executing a query:
{ "data": { "course": { "__typename": "Course", "id": "Q291cnNlOjQ=", "title": "Training Course", "location": { "__typename": "Location", "id": "TG9jYXRpb246Mg==", "name": "London", } } } }
In this example the client will identify the
Course
andLocation
objects. -
Next, the client requires a unique identifier for each object in the cache. The default method it uses is to concatenate the
__typename
andid
of the object. So for our example, the cache ID for each object would be:Course:Q291cnNlOjQ=
andLocation:TG9jYXRpb246Mg==
. -
Now that the client has a unique ID for each cache object it can store them in a flat lookup table. Objects stored in the cache will have the values of fields that contain other objects replaced with references to that object. Below is what our example response object would like once cached:
{ "data": { "course": { "__typename": "Course", "id": "Q291cnNlOjQ=", "title": "Training Course", "location": { "ref": "Location:TG9jYXRpb246Mg==", } } } }
If we then request a different
Course
object that takes place at the same location, each cachedCourse
object will reference the same cachedLocation
object. -
The client executes this caching process every time we receive query response data. If the cache finds an object with the same ID it will merge the new object with the cache object, overwriting common fields and preserving unique ones.
Its very important to fetch the unique identifiers of a type when executing a query, otherwise there will be issues with the caching process. The client is helpful and will automatically include the
__typename
field for you on response objects. But, if using the client’s default method for creating unique identifiers, always make sure to query theid
field on types!
Fetch policies
Apollo Client’s cache is very clever and is very customizable. This can sometimes make it difficult to understand where your data is coming from. To assist in these circumstances the client allows you to set fetch policies on queries. Each policy specifies how queries go about fetching their data which can give you a better understanding of where your data is coming from and help you troubleshoot issues when the data you receive is unexpected.
There are several fetch policies to choose from but we’ll just discuss the four most useful. The rest can be read about here.
A fetch policy can be specified as a parameter when using Apollo Client’s useQuery
hook like below:
const { data, loading, error } = useQuery(GET_COURSES, {
fetchPolicy: "no-cache",
});
Cache first
cache-first
This is the default fetch policy. When querying the client will first check if the data is in the cache. If all the data is present it will return it right away. Otherwise, it will fetch the data from your GraphQL API. This policy is best used when you want to minimize network requests from your application.
Cache and network
cache-and-network
With this policy enabled the client will simultaneously query data from the cache and the API. When the requested data is received from the API, if the cached data is modified, the query result will be automatically updated. This policy provides a fast response but keeps cached data consistent with data stored on the server.
Network only
network-only
This policy will bypass the cache and only query data from the API. However, it will store the response data in the cache. This policy prioritises up-to-date data over fast responses but stores the data in the cache if you need it in the future.
No cache
no-cache
The behaviour of this policy is the same as network-only
except it does not store response data in the cache.
We recently encountered a scenario where we needed to use the no-cache
fetch policy. One of our frontend services is split between some legacy Angular code and more recent React code. The React code utilised Apollo Client whereas the legacy code did not. When firing a mutation from the legacy code to modify some data, the data was not being updated on the page. Because the mutation was not executed by Apollo Client, the cache was not updated and our query returned the old data that was stored there. By using this policy we always queried the data from the API instead and users were able to see the updated data on the page as they expected.
Optimistic results
Apollo Client is able to optimistically update an application’s UI when creating or updating data. In practice, this means that when running a mutation the client can predict what the result will be and display it to the user. If this prediction is wrong, the UI will automatically update to reflect the actual response once it is received from the GraphQL server. This functionality is built on the power of Apollo Client’s cache.
When a mutation is called, the cache will store an optimistic version of the updated object in the cache. It does not overwrite the existing cached object - just in case the optimistic response turns out to be wrong! Next, Apollo Client will notify and automatically update any queries that include the updated object. This results in their associated components re-rendering with the updated data almost instantly. When the GraphQL server responds with the actual updated object it will remove the optimistic version from the cache and overwrite the canonical object. This will cause another re-render but if the optimistic result was correct the user won’t be able to tell!
An optimistic result can be included by providing an optimisticResponse
option to the mutate
function returned by useMutation
. Here’s an example:
mutate({
variables: {
courseId,
title,
},
optimisticResponse: {
updateCourse: {
id: courseId,
__typename: "Course",
title: title,
},
},
});
The
optimisticResponse
object needs to match the shape of the mutation response we expect from our server. It is also important to include theid
and__typename
fields for the cache to use as the object’s unique identifier.
You can also provide an optimistic result when creating a new object. In this scenario, you need to provide a temporary id
value. Once the server responds, the optimistic object will be removed from the cache as usual and replaced with the actual object.
This post introduced the basic concepts of Apollo Client’s cache and how to get it working in your favour. For more information on its capabilities and how to customize its functionality, the official documentation is a great place to start!