Reading and writing data to the cache
Apollo Client provides the following methods for reading and writing data to the cache:
readQuery
andreadFragment
writeQuery
andwriteFragment
cache.modify
(a method ofInMemoryCache
)
These methods are described in detail below.
All code samples below assume that you have initialized an instance of ApolloClient
and that you have imported the gql
function from @apollo/client
.
readQuery
The readQuery
method enables you to run a GraphQL query directly on your cache.
- If your cache contains all of the data necessary to fulfill a specified query,
readQuery
returns a data object in the shape of that query, just like a GraphQL server does. - If your cache doesn't contain all of the data necessary to fulfill a specified query,
readQuery
throws an error. It never attempts to fetch data from a remote server.
Pass readQuery
a GraphQL query string like so:
const { todo } = client.readQuery({
query: gql`
query ReadTodo {
todo(id: 5) {
id
text
completed
}
}
`,
});
You can provide GraphQL variables to readQuery
like so:
const { todo } = client.readQuery({
query: gql`
query ReadTodo($id: Int!) {
todo(id: $id) {
id
text
completed
}
}
`,
variables: {
id: 5,
},
});
Do not modify the return value of
readQuery
. The same object might be returned to multiple components. To update data in the cache, instead create a replacement object and pass it towriteQuery
.
readFragment
The readFragment
method enables you to read data from any normalized cache object that was stored as part of any query result. Unlike with readQuery
, calls to readFragment
do not need to conform to the structure of one of your data graph's supported queries.
Here's an example that fetches a particular item from a to-do list:
const todo = client.readFragment({
id: '5', // The value of the to-do item's unique identifier
fragment: gql`
fragment MyTodo on Todo {
id
text
completed
}
`,
});
The first argument, id
, is the value of the unique identifier for the object you want to read from the cache. By default, this is the value of the object's id
field, but you can customize this behavior.
In the example above:
- If a
Todo
object with anid
of5
is not in the cache,readFragment
returnsnull
. - If the
Todo
object is in the cache but it's missing either atext
orcompleted
field,readFragment
throws an error.
writeQuery
and writeFragment
In addition to reading arbitrary data from the Apollo Client cache, you can write arbitrary data to the cache with the writeQuery
and writeFragment
methods.
Any changes you make to cached data with
writeQuery
andwriteFragment
are not pushed to your GraphQL server. If you reload your environment, these changes will disappear.
These methods have the same signature as their read
counterparts, except they require an additional data
variable.
For example, the following call to writeFragment
locally updates the completed
flag for a Todo
object with an id
of 5
:
client.writeFragment({
id: '5',
fragment: gql`
fragment MyTodo on Todo {
completed
}
`,
data: {
completed: true,
},
});
All subscribers to the Apollo Client cache (including all active queries) see this change and update your application's UI accordingly.
Combining reads and writes
Combine readQuery
and writeQuery
to fetch currently cached data and make selective modifications to it. The example below creates a new Todo
item and adds it your cached to-do list. Remember, this addition is not sent to your remote server.
// Query that fetches all existing to-do items
const query = gql`
query MyTodoAppQuery {
todos {
id
text
completed
}
}
`;
// Get the current to-do list
const data = client.readQuery({ query });
// Create a new to-do item
const myNewTodo = {
id: '6',
text: 'Start using Apollo Client.',
completed: false,
__typename: 'Todo',
};
// Write back to the to-do list, appending the new item
client.writeQuery({
query,
data: {
todos: [...data.todos, myNewTodo],
},
});
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). - Unlike
writeQuery
andwriteFragment
,modify
circumvents anymerge
functions you've defined, which means that fields are always overwritten with exactly the values you specify.
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:
cache.modify({
id: cache.identify(myObject),
fields: {
name(cachedName) {
return cachedName.toUpperCase();
},
},
/* broadcast: false // Include this to prevent automatic query refresh */
});
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 identifier. 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.
Modifier function utilities
A modifier function can optionally take a second parameter, which is an object that contains several helpful utilities.
A couple of these utilities (the readField
function and the DELETE
sentinel object) are used in the examples below. For descriptions of all available utilities, see the API reference.
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:
const idToRemove = 'abc123';
cache.modify({
id: cache.identify(myPost),
fields: {
comments(existingCommentRefs, { readField }) {
return existingCommentRefs.filter(
commentRef => idToRemove !== readField('id', commentRef)
);
},
},
});
Let's break this down:
- In the
id
field, we usecache.identify
to obtain the identifier 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
:
const newComment = {
__typename: 'Comment',
id: 'abc123',
text: 'Great blog post!',
};
cache.modify({
fields: {
comments(existingCommentRefs = [], { readField }) {
const newCommentRef = cache.writeFragment({
data: newComment,
fragment: gql`
fragment NewComment on Comment {
id
text
}
`
});
// Quick safety check - if the new comment is already
// present in the cache, we don't need to add it again.
if (existingCommentRefs.some(
ref => readField('id', ref) === newComment.id
)) {
return existingCommentRefs;
}
return [...existingCommentRefs, newCommentRef];
}
}
});
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 data that's identical (===
) to an existing object in the cache, it returns a reference to the existing object without writing any new data. This means we can use writeFragment
to obtain a reference to an existing object in the cache. This can come in handy when using Apollo Client features like useMutation
, which might have already added data we're interested in working with.
For example:
const [addComment] = useMutation(ADD_COMMENT, {
update(cache, { data: { addComment } }) {
cache.modify({
fields: {
comments(existingCommentRefs = [], { readField }) {
const newCommentRef = cache.writeFragment({
data: addComment,
fragment: gql`
fragment NewComment on Comment {
id
text
}
`
});
return [...existingCommentRefs, newCommentRef];
}
}
});
}
});
In this example, useMutation
creates a Comment
and automatically 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:
cache.modify({
id: cache.identify(myPost),
fields: {
comments(existingCommentRefs, { DELETE }) {
return DELETE;
},
},
});
Obtaining an object's custom ID
If a type in your cache uses a custom identifier (or even if it doesn't), you can use the cache.identify
method to obtain the identifier for an object of that type. This method takes an object and computes its ID based on both its __typename
and its identifier field(s). This means you don't have to keep track of which fields make up each type's identifier.
Example
Let's say we have a JavaScript representation of a cached GraphQL object, like this:
const invisibleManBook = {
__typename: 'Book',
isbn: '9780679601395', // This type's custom identifier title: 'Invisible Man',
author: {
__typename: 'Author',
name: 'Ralph Ellison',
},
};
If we want to interact with this object in our cache with methods like writeFragment
or cache.modify
, we need the object's identifier. Our Book
type's identifier appears to be custom, because the id
field isn't present.
Instead of needing to look up that our Book
type uses the isbn
field as its identifier, we can use the cache.identify
method, like so:
const bookYearFragment = gql`
fragment BookYear on Book {
publicationYear
}
`;
const fragmentResult = cache.writeFragment({
id: cache.identify(invisibleManBook), fragment: bookYearFragment,
data: {
publicationYear: '1952'
}
});
The cache knows that the Book
type uses the isbn
field for its identifier, so cache.identify
can correctly populate the id
field above.
This example is straightforward because our custom identifier uses a single field (isbn
). But custom identifiers can consist of multiple fields (such as both isbn
and title
). This makes it much more challenging and repetitive to specify an object's custom ID without using cache.identify
.