Docs
Launch GraphOS Studio

Reading and writing data to the cache


You can read and write data directly to the cache, without communicating with your . You can interact with data that you previously fetched from your server, and with data that's only available locally.

supports multiple strategies for interacting with cached data:

StrategyAPIDescription
Using GraphQL queriesreadQuery / writeQuery / updateQueryUse standard GraphQL queries for managing both remote and local data.
Using GraphQL fragmentsreadFragment / writeFragment / updateFragment / useFragmentAccess the fields of any cached object without composing an entire query to reach that object.
Directly modifying cached fieldscache.modifyManipulate 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 that contain references to other objects in the cache and fields that contain literal values. References are objects that contain a __ref —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 the gql function from @apollo/client. If you haven't, get started.

In a React component, you can access your instance of ApolloClient using ApolloProvider and the useApolloClient hook.

Using GraphQL queries

You can read and write cache data using queries that are similar (or even identical) to queries that you execute on your server:

readQuery

The readQuery method enables you to execute a directly on your cache, like so:

const READ_TODO = gql`
query ReadTodo($id: ID!) {
todo(id: $id) {
id
text
completed
}
}
`;
// Fetch the cached to-do item with ID 5
const { todo } = client.readQuery({
query: READ_TODO,
// Provide any required variables in this object.
// Variables of mismatched types will return `null`.
variables: {
id: 5,
},
});

If your cache contains data for all of the 's , readQuery returns an object that matches the shape of the .

To successfully execute queries with , the with the specified must already be in the cache. In the above example, to get the to-do item with the id of 5, the todo (s) for id:5 must already be cached. For this example, the cache would need to look something like this:

{
ROOT_QUERY: {
'todo({"id":5})': {
__ref: 'Todo:5'
}
},
'Todo:5': {
// ...
}
}

Otherwise the client treats the data as missing and readQuery returns null. To learn more about how the caching works, checkout the caching overview.

{
todo: {
__typename: 'Todo', // __typename is automatically included
id: 5,
text: 'Buy oranges 🍊',
completed: true
}
}

automatically queries for every object's __typename by default, even if you don't include this in your 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 's , readQuery returns null. It does not attempt to fetch data from your .

The you provide readQuery can include that are not defined in your 's schema (i.e., local-only fields).

Prior to 3.3, readQuery threw a MissingFieldError exception to report missing . Beginning with 3.3, readQuery always returns null to indicate that are missing.

writeQuery

The writeQuery method enables you to write data to your cache in a shape that matches a . It resembles readQuery, except that it requires a data option:

client.writeQuery({
query: gql`
query WriteTodo($id: Int!) {
todo(id: $id) {
id
text
completed
}
}`,
data: { // Contains the data to write
todo: {
__typename: 'Todo',
id: 5,
text: 'Buy grapes 🍇',
completed: false
},
},
variables: {
id: 5
}
});

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 . If you reload your environment, these changes disappear.
  • The shape of your is not enforced by your 's schema:
    • The can include that are not present in your schema.
    • You can (but usually shouldn't) provide values for schema 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 that are included in data (other are preserved):

// BEFORE
{
'Todo:5': {
__typename: 'Todo',
id: 5,
text: 'Buy oranges 🍊',
completed: true,
dueDate: '2022-07-02'
}
}
// AFTER
{
'Todo:5': {
__typename: 'Todo',
id: 5,
text: 'Buy grapes 🍇',
completed: false,
dueDate: '2022-07-02'
}
}

If you include a in query but don't include a value for it in data, the 's current cached value is preserved.

Using GraphQL fragments

You can read and write cache data using on any normalized cache object. This provides more "random access" to your cached data than readQuery/writeQuery, which require a complete valid .

readFragment

This example fetches the same data as the example for readQuery using readFragment instead:

const todo = client.readFragment({
id: 'Todo:5', // The value of the to-do item's cache ID
fragment: gql`
fragment MyTodo on Todo {
id
text
completed
}
`,
});

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 ID 5.
  • There is a cached Todo object with ID 5, but it's missing a value for either text or completed.

Prior to 3.3, readFragment threw MissingFieldError exceptions to report missing , and returned null only when reading a from a nonexistent ID. Beginning with 3.3, readFragment always returns null to indicate insufficient data (missing ID or missing ), instead of throwing a MissingFieldError.

writeFragment

In addition to reading "random-access" data from the 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 . For example, the following call to writeFragment updates the completed flag for a Todo object with an id of 5:

client.writeFragment({
id: 'Todo:5',
fragment: gql`
fragment MyTodo on Todo {
completed
}
`,
data: {
completed: true,
},
});

All subscribers to the cache (including all active queries) see this change and update your application's UI accordingly.

useFragment
Since 3.8.0

You can read data for a given 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 . 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.

As a convenience, you can use cache.updateQuery or cache.updateFragment to combine reading and writing cached data with a single method call:

// Query to fetch all todo items
const query = gql`
query MyTodoAppQuery {
todos {
id
text
completed
}
}
`;
// Set all todos in the cache as completed
cache.updateQuery({ query }, (data) => ({
todos: data.todos.map((todo) => ({ ...todo, completed: true }))
}));

Each of these methods takes two parameters:

  • The same options parameter as its read method counterpart (which always includes a query or fragment)
  • 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 set to true (and other 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 and cache.updateFragment.

Using cache.modify

The modify method of InMemoryCache enables you to directly modify the values of individual cached , or even delete fields entirely.

  • Like writeQuery and writeFragment, modify triggers a refresh of all active queries that depend on modified (unless you override this behavior by passing broadcast: false).
  • Unlike writeQuery and writeFragment:
    • modify circumvents any merge functions you've defined, which means that are always overwritten with exactly the values you specify.
    • modify cannot write 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 and nextFetchPolicy to client.watchQuery or the useQuery hook.

Parameters

Canonically 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 to modify)
  • Optional broadcast and optimistic boolean values to customize behavior

A modifier function applies to a single . 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 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 , that field's cached value remains unchanged.

Values vs. references

When you define a modifier function for a that contains a , 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 that has current value 5, your modifier function is passed the value 5.

However, when you define a modifier function for a that contains an 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 . You don't modify the original cached object's data. Additionally, modifying a (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 Comments. 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 , we use cache.identify to obtain the cache ID of the cached Post object we want to remove a comment from.

  • In the fields , we provide an object that lists our modifier functions. In this case, we define a single modifier function (for the comments ).

  • The comments modifier function takes our existing cached array of comments as a parameter (existingCommentRefs). It also uses the readField utility function, which helps you read the value of any cached .

  • 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({
id: cache.identify(myPost),
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 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 ), 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:

const [addComment] = useMutation(ADD_COMMENT, {
update(cache, { data: { addComment } }) {
cache.modify({
id: cache.identify(myPost),
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 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 from a particular cached object, return the DELETE sentinel object from the 's modifier function, like so:

cache.modify({
id: cache.identify(myPost),
fields: {
comments(existingCommentRefs, { DELETE }) {
return DELETE;
},
},
});

Example: Invalidating fields within a cached object

Normally, changing or deleting a 's value also invalidates the , causing watched queries to be reread if they previously consumed the field.

Using cache.modify, it's also possible to invalidate the without changing or deleting its value, by returning the INVALIDATE sentinel:

cache.modify({
id: cache.identify(myPost),
fields: {
comments(existingCommentRefs, { INVALIDATE }) {
return INVALIDATE;
},
},
});

If you need to invalidate all within a given object, you can pass a modifier function as the value of the fields option:

cache.modify({
id: cache.identify(myPost),
fields(fieldValue, details) {
return details.INVALIDATE;
},
});

When using this form of cache.modify, you can determine individual names using details.fieldName. This technique works for any modifier function, not just those that return INVALIDATE.

Obtaining an object's cache ID

If a type in your cache uses a custom cache ID (or even if it doesn't), you can use the cache.identify method to obtain the cache ID for an object of that type. This method takes an object and computes its ID based on both its __typename and its identifier (s). This means you don't have to keep track of which fields make up each type's cache ID.

Example

Let's say we have a JavaScript representation of a cached object, like this:

const invisibleManBook = {
__typename: 'Book',
isbn: '9780679601395', // The key field for this type's cache ID
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 cache ID. Our Book type's cache ID appears to be custom, because the id isn't present.

Instead of needing to look up that our Book type uses the isbn for its cache ID, 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 for its cache ID, so cache.identify can correctly populate the id above.

This example is straightforward because our cache ID uses a single (isbn). But custom cache IDs can consist of multiple (such as both isbn and title). This makes it much more challenging and repetitive to specify an object's cache ID without using cache.identify.

Previous
Configuration
Next
Garbage collection and eviction
Edit on GitHubEditForumsDiscord

© 2024 Apollo Graph Inc.

Privacy Policy

Company