Reading and writing data to the cache
You can read and write data directly to the Apollo Client cache, without communicating with your GraphQL server. You can interact with data that you previously fetched from your server, and with data that's only available locally .
Apollo Client supports multiple strategies for interacting with cached data:
Strategy | API | Description |
---|---|---|
Using GraphQL queries | readQuery / writeQuery / updateQuery | Use standard GraphQL queries for managing both remote and local data. |
Using GraphQL fragments | readFragment / writeFragment / updateFragment / useFragment | Access the fields of any cached object without composing an entire query to reach that object. |
Directly modifying cached fields | cache.modify | Manipulate cached data without using GraphQL at all. |
You can use whichever combination of strategies and methods are most helpful for your use case.
Bear in mind the difference between fields that contain references to other objects in the cache and fields that contain literal values. References are objects that contain a __ref
field—see the example in the caching overview. Modifying a reference will not change the values contained in the object to which the reference points. So avoid updating an object from something like {__ref: '5'}
to {__ref: '5', completed: true}
.
All code samples below assume that you have initialized an instance of
ApolloClient
and that you have imported thegql
function from@apollo/client
. If you haven't, get started .In a React component, you can access your instance of
ApolloClient
usingApolloProvider
and theuseApolloClient
hook.
Using GraphQL queries
You can read and write cache data using GraphQL queries that are similar (or even identical) to queries that you execute on your server:
readQuery
The readQuery
method enables you to execute a GraphQL query directly on your cache, like so:
1const READ_TODO = gql`
2 query ReadTodo($id: ID!) {
3 todo(id: $id) {
4 id
5 text
6 completed
7 }
8 }
9`;
10
11// Fetch the cached to-do item with ID 5
12const { todo } = client.readQuery({
13 query: READ_TODO,
14 // Provide any required variables in this object.
15 // Variables of mismatched types will return `null`.
16 variables: {
17 id: 5,
18 },
19});
If your cache contains data for all of the query's fields, readQuery
returns an object that matches the shape of the query.
To successfully execute queries with variables, the field with the specified argument must already be in the cache. In the above example, to get the to-do item with the id
of 5
, the todo
field(s) for id:5
must already be cached. For this example, the cache would need to look something like this:
1{
2 ROOT_QUERY: {
3 'todo({"id":5})': {
4 __ref: 'Todo:5'
5 }
6 },
7 'Todo:5': {
8 // ...
9 }
10}
Otherwise the client treats the data as missing and readQuery
returns null
. To learn more about how the caching works, checkout the caching overview .
1{
2 todo: {
3 __typename: 'Todo', // __typename is automatically included
4 id: 5,
5 text: 'Buy oranges 🍊',
6 completed: true
7 }
8}
Apollo Client automatically queries for every object's
__typename
by default, even if you don't include this field in your query string.
Do not modify the returned object directly. The same object might be returned to multiple components. To update cached data safely, see Combining reads and writes .
If the cache is missing data for any of the query's fields, readQuery
returns null
. It does not attempt to fetch data from your GraphQL server.
The query you provide readQuery
can include fields that are not defined in your GraphQL server's schema (i.e., local-only fields ).
Prior to Apollo Client 3.3,
readQuery
threw aMissingFieldError
exception to report missing fields. Beginning with Apollo Client 3.3,readQuery
always returnsnull
to indicate that fields are missing.
writeQuery
The writeQuery
method enables you to write data to your cache in a shape that matches a GraphQL query. It resembles readQuery
, except that it requires a data
option:
1client.writeQuery({
2 query: gql`
3 query WriteTodo($id: Int!) {
4 todo(id: $id) {
5 id
6 text
7 completed
8 }
9 }`,
10 data: { // Contains the data to write
11 todo: {
12 __typename: 'Todo',
13 id: 5,
14 text: 'Buy grapes 🍇',
15 completed: false
16 },
17 },
18 variables: {
19 id: 5
20 }
21});
This example creates (or edits) a cached Todo
object with ID 5
.
Note the following about writeQuery
:
Any changes you make to cached data with
writeQuery
are not pushed to your GraphQL server. If you reload your environment, these changes disappear.The shape of your query is not enforced by your GraphQL server's schema:
The query can include fields that are not present in your schema.
You can (but usually shouldn't) provide values for schema fields that are invalid according to your schema.
Editing existing data
In the example above, if your cache already contains a Todo
object with ID 5
, writeQuery
overwrites the fields that are included in data
(other fields are preserved):
1// BEFORE
2{
3 'Todo:5': {
4 __typename: 'Todo',
5 id: 5,
6 text: 'Buy oranges 🍊',
7 completed: true,
8 dueDate: '2022-07-02'
9 }
10}
11
12// AFTER
13{
14 'Todo:5': {
15 __typename: 'Todo',
16 id: 5,
17 text: 'Buy grapes 🍇',
18 completed: false,
19 dueDate: '2022-07-02'
20 }
21}
If you include a field in
query
but don't include a value for it indata
, the field's current cached value is preserved.
Using GraphQL fragments
You can read and write cache data using GraphQL fragments on any normalized cache object. This provides more "random access" to your cached data than readQuery
/writeQuery
, which require a complete valid query.
readFragment
This example fetches the same data as the example for readQuery
using readFragment
instead:
1const todo = client.readFragment({
2 id: 'Todo:5', // The value of the to-do item's cache ID
3 fragment: gql`
4 fragment MyTodo on Todo {
5 id
6 text
7 completed
8 }
9 `,
10});
Unlike readQuery
, readFragment
requires an id
option. This option specifies the cache ID for the object in your cache. By default , cache IDs have the format <__typename>:<id>
(which is why we provide Todo:5
above). You can customize this ID .
In the example above, readFragment
returns null
in either of the following cases:
There is no cached
Todo
object with ID5
.There is a cached
Todo
object with ID5
, but it's missing a value for eithertext
orcompleted
.
Prior to Apollo Client 3.3,
readFragment
threwMissingFieldError
exceptions to report missing fields, and returnednull
only when reading a fragment from a nonexistent ID. Beginning with Apollo Client 3.3,readFragment
always returnsnull
to indicate insufficient data (missing ID or missing fields), instead of throwing aMissingFieldError
.
writeFragment
In addition to reading "random-access" data from the Apollo Client cache with readFragment
, you can write data to the cache with the writeFragment
method.
Any changes you make to cached data with
writeFragment
are not pushed to your GraphQL server. If you reload your environment, these changes will disappear.
The writeFragment
method resembles readFragment
, except it requires an additional data
variable. For example, the following call to writeFragment
updates the completed
flag for a Todo
object with an id
of 5
:
1client.writeFragment({
2 id: 'Todo:5',
3 fragment: gql`
4 fragment MyTodo on Todo {
5 completed
6 }
7 `,
8 data: {
9 completed: true,
10 },
11});
All subscribers to the Apollo Client cache (including all active queries) see this change and update your application's UI accordingly.
watchFragment
Since 3.10.0
Watches the cache store of the fragment according to the options specified and returns an Observable
. We can subscribe to this Observable
and receive updated results through an observer when the cache store changes.
You must pass in a GraphQL document with a single fragment or a document with multiple fragments that represent what you are reading. If you pass in a document with multiple fragments then you must also specify a fragmentName
.
useFragment
Since 3.8.0
You can read data for a given fragment directly from the cache using the useFragment
hook. This hook returns an always-up-to-date view of whatever data the cache currently contains for a given fragment. See the API reference.
Combining reads and writes
You can combine readQuery
and writeQuery
(or readFragment
and writeFragment
) to fetch currently cached data and make selective modifications to it. The following example creates a new Todo
item and adds it to your cached to-do list. Remember, this addition is not sent to your remote server.
Click to expand
1// Query that fetches all existing to-do items
2const query = gql`
3 query MyTodoAppQuery {
4 todos {
5 id
6 text
7 completed
8 }
9 }
10`;
11
12// Get the current to-do list
13const data = client.readQuery({ query });
14
15// Create a new to-do item
16const myNewTodo = {
17 id: '6',
18 text: 'Start using Apollo Client.',
19 completed: false,
20 __typename: 'Todo',
21};
22
23// Write back to the to-do list, appending the new item
24client.writeQuery({
25 query,
26 data: {
27 todos: [...data.todos, myNewTodo],
28 },
29});
Using updateQuery
and updateFragment
Since 3.5
As a convenience, you can use cache.updateQuery
or cache.updateFragment
to combine reading and writing cached data with a single method call:
1// Query to fetch all todo items
2const query = gql`
3 query MyTodoAppQuery {
4 todos {
5 id
6 text
7 completed
8 }
9 }
10`;
11
12// Set all todos in the cache as completed
13cache.updateQuery({ query }, (data) => ({
14 todos: data.todos.map((todo) => ({ ...todo, completed: true }))
15}));
Each of these methods takes two parameters:
The same
options
parameter as itsread
method counterpart (which always includes aquery
orfragment
)An update function
After either method fetches data from the cache, it calls its update function and passes it the cached data
. The update function can then return a value to replace that data
in the cache. In the example above, every cached Todo
object has its completed
field set to true
(and other fields remain unchanged).
Please note that the replacement value has to be calculated in an immutable way. You can read more about immutable updates in the React documentation .
If the update function shouldn't make any changes to the cached data, it can return undefined
.
The update function's return value is passed to either writeQuery
or writeFragment
, which modifies the cached data.
See the full API reference for
cache.updateQuery
andcache.updateFragment
.
Using cache.modify
The modify
method of InMemoryCache
enables you to directly modify the values of individual cached fields, or even delete fields entirely.
Like
writeQuery
andwriteFragment
,modify
triggers a refresh of all active queries that depend on modified fields (unless you override this behavior by passingbroadcast: false
).Unlike
writeQuery
andwriteFragment
:modify
circumvents anymerge
functions you've defined, which means that fields are always overwritten with exactly the values you specify.modify
cannot write fields that do not already exist in the cache.
Watched queries can control what happens when they're invalidated by updates to the cache, by passing options like
fetchPolicy
andnextFetchPolicy
toclient.watchQuery
or theuseQuery
hook.
Parameters
Canonically documented in the API reference , the modify
method takes the following parameters:
The ID of a cached object to modify (which we recommend obtaining with
cache.identify
)A map of modifier functions to execute (one for each field to modify)
Optional
broadcast
andoptimistic
boolean values to customize behavior
A modifier function applies to a single field. It takes its associated field's current cached value as a parameter and returns whatever value should replace it.
Here's an example call to modify
that modifies a name
field to convert its value to upper case:
1cache.modify({
2 id: cache.identify(myObject),
3 fields: {
4 name(cachedName) {
5 return cachedName.toUpperCase();
6 },
7 },
8 /* broadcast: false // Include this to prevent automatic query refresh */
9});
If you don't provide a modifier function for a particular field, that field's cached value remains unchanged.
Values vs. references
When you define a modifier function for a field that contains a scalar, an enum, or a list of these base types, the modifier function is passed the exact existing value for the field. For example, if you define a modifier function for an object's quantity
field that has current value 5
, your modifier function is passed the value 5
.
However, when you define a modifier function for a field that contains an object type or a list of objects, those objects are represented as references. Each reference points to its corresponding object in the cache by its cache ID. If you return a different reference in your modifier function, you change which other cached object is contained in this field. You don't modify the original cached object's data. Additionally, modifying a field (or adding a new one) in a reference will only take effect for the location you're modifying.
Modifier function utilities
A modifier function can optionally take a second parameter, which is an object that contains several helpful utilities.
Some of these utilities (namely, the readField
function and the DELETE
sentinel object) are used in the examples below. For descriptions of all available utilities, see the API reference .
Examples
Example: Removing an item from a list
Let's say we have a blog application where each Post
has an array of Comment
s. Here's how we might remove a specific Comment
from a paginated Post.comments
array:
1const idToRemove = 'abc123';
2
3cache.modify({
4 id: cache.identify(myPost),
5 fields: {
6 comments(existingCommentRefs, { readField }) {
7 return existingCommentRefs.filter(
8 commentRef => idToRemove !== readField('id', commentRef)
9 );
10 },
11 },
12});
Let's break this down:
In the
id
field, we usecache.identify
to obtain the cache ID of the cachedPost
object we want to remove a comment from.In the
fields
field, we provide an object that lists our modifier functions. In this case, we define a single modifier function (for thecomments
field).The
comments
modifier function takes our existing cached array of comments as a parameter (existingCommentRefs
). It also uses thereadField
utility function, which helps you read the value of any cached field.The modifier function returns an array that filters out all comments with an ID that matches
idToRemove
. The returned array replaces the existing array in the cache.
Example: Adding an item to a list
Now let's look at adding a Comment
to a Post
:
1const newComment = {
2 __typename: 'Comment',
3 id: 'abc123',
4 text: 'Great blog post!',
5};
6
7cache.modify({
8 id: cache.identify(myPost),
9 fields: {
10 comments(existingCommentRefs = [], { readField }) {
11 const newCommentRef = cache.writeFragment({
12 data: newComment,
13 fragment: gql`
14 fragment NewComment on Comment {
15 id
16 text
17 }
18 `
19 });
20
21 // Quick safety check - if the new comment is already
22 // present in the cache, we don't need to add it again.
23 if (existingCommentRefs.some(
24 ref => readField('id', ref) === newComment.id
25 )) {
26 return existingCommentRefs;
27 }
28
29 return [...existingCommentRefs, newCommentRef];
30 }
31 }
32});
When the comments
field modifier function is called, it first calls writeFragment
to store our newComment
data in the cache. The writeFragment
function returns a reference (newCommentRef
) that points to the newly cached comment.
As a safety check, we then scan the array of existing comment references (existingCommentRefs
) to make sure that our new isn't already in the list. If it isn't, we add the new comment reference to the list of references, returning the full list to be stored in the cache.
Example: Updating the cache after a mutation
If you call writeFragment
with an options.data
object that the cache is able to identify ( based on its __typename
and cache ID fields), you can avoid passing options.id
to writeFragment
.
Whether you provide options.id
explicitly or let writeFragment
figure it out using options.data
, writeFragment
returns a Reference
to the identified object.
This behavior makes writeFragment
a good tool for obtaining a Reference
to an existing object in the cache, which can come in handy when writing an update
function for useMutation
:
For example:
1const [addComment] = useMutation(ADD_COMMENT, {
2 update(cache, { data: { addComment } }) {
3 cache.modify({
4 id: cache.identify(myPost),
5 fields: {
6 comments(existingCommentRefs = [], { readField }) {
7 const newCommentRef = cache.writeFragment({
8 data: addComment,
9 fragment: gql`
10 fragment NewComment on Comment {
11 id
12 text
13 }
14 `
15 });
16 return [...existingCommentRefs, newCommentRef];
17 }
18 }
19 });
20 }
21});
In this example, useMutation
automatically creates a Comment
and adds it to the cache, but it doesn't automatically know how to add that Comment
to the corresponding Post
's list of comments
. This means that any queries watching the Post
's list of comments
won't update.
To address this, we use the update
callback of useMutation
to call cache.modify
. Like the previous example , we add the new comment to the list. Unlike the previous example, the comment was already added to the cache by useMutation
. Consequently, cache.writeFragment
returns a reference to the existing object.
Example: Deleting a field from a cached object
A modifier function's optional second parameter is an object that includes several helpful utilities , such as the canRead
and isReference
functions. It also includes a sentinel object called DELETE
.
To delete a field from a particular cached object, return the DELETE
sentinel object from the field's modifier function, like so:
1cache.modify({
2 id: cache.identify(myPost),
3 fields: {
4 comments(existingCommentRefs, { DELETE }) {
5 return DELETE;
6 },
7 },
8});
Example: Invalidating fields within a cached object
Normally, changing or deleting a field's value also invalidates the field, causing watched queries to be reread if they previously consumed the field.
Using cache.modify
, it's also possible to invalidate the field without changing or deleting its value, by returning the INVALIDATE
sentinel:
1cache.modify({
2 id: cache.identify(myPost),
3 fields: {
4 comments(existingCommentRefs, { INVALIDATE }) {
5 return INVALIDATE;
6 },
7 },
8});
If you need to invalidate all fields within a given object, you can pass a modifier function as the value of the fields
option: