Apollo Client’s new imperative store API
Caleb Meredith
GraphQL is strongest when it is a unified access point to your organization’s entire data universe: You may have a plethora of micro-services churning away in the background, but your GraphQL consumer doesn’t need to know those details. Instead, they can focus on being productive using your organization’s single, cohesive, data model.
Often, an app’s entire data universe is way too large to reasonably download and manage entirely in an app, but at the same time we want to fool our users into thinking that we’ve done just that. To build an app that creates this kind of user perception, it is not enough to just layer on some consistency features at the end of the development cycle — you must fundamentally build paradigms into the application’s infrastructure that allow your front-end engineers to efficiently operate a subset of your data universe locally.
This is where a GraphQL client comes in. A GraphQL client should allow you to take a slice of your GraphQL data universe and make it accessible on the client. Clients like Apollo store your data in a normalized form where the id is computed by the user defined function dataIdFromObject
. In this way, Apollo Client acts very similarly to a database. It saves data using a query language, and provides an interface for you to read that data back out.
However, before Apollo Client 0.10.0 there were only two ways for you to access and modify the data in this database: You could try reading data with a query by settingnoFetch
, or writing data with a call toupdateQueries
. While these features are great in the context for which they were designed, they were not built to offer complete control over the client-side cache.
Instead, Apollo Client now provides the ability to control the store with four new methods: readQuery()
, readFragment()
, writeQuery()
, and writeFragment()
.
These methods are available on your ApolloClient
instance and allow you to read and write directly to your cache. Together, they will allow you to provide a compelling experience to your users by empowering you to update the data in your cache in any way you choose. These methods expose enough power that you may, in fact, be able to write your own customized GraphQL client API on top of them!
Let’s dive into the details of each method and how you may use them together to build great experiences.
Reading Queries From the Store
The first method for interacting directly with your cache is readQuery()
. This method will read data from your cache starting at your root query type. To use the method, you provide it the query you want to read as a named argument like so:
const data = client.readQuery({
query: gql`
{
todo(id: 1) {
id
text
completed
}
}
`,
});
If the data exists in your cache then it will be returned and you may interact with the data
object however you like! If not, all of the data exists in your store the query can not be fulfilled so an error will be thrown.
You may use a query from anywhere in your app with this method. You may also pass in variables:
import { TodoQuery } from './TodoGraphQL';
const data = client.readQuery({
query: TodoQuery,
variables: { id: 5 },
});
readQuery()
is similar to the existing query()
method on Apollo Client, except that readQuery()
will never send a request using your network interface. It will always try to read from only the cache, and if that read fails then an error will be thrown.
Reading Fragments From the Store
Sometimes, however, you want to read from an arbitrary point in your store and not just from your root query type. For that there is the new readFragment()
method. This method accepts a GraphQL fragment and an id and returns you the data at that id matching the provided fragment:
client.readFragment({
id: '5',
fragment: gql`
fragment todo on Todo {
id
text
completed
}
`,
});
The id
should be a string that is returned by the dataIdFromObject
function you defined when initializing an instance of ApolloClient
. For instance, if you use this common dataIdFromObject
function:
const client = new ApolloClient({
dataIdFromObject: o => {
if (o.__typename != null && o.id != null) {
return `${o.__typename}-${o.id}`;
}
},
});
Then your id might be Todo5
instead of just 5
because you added the __typename
to beginning of the id.
Writing Queries and Fragments To the Store
Both readQuery()
and readFragment()
have analogous methods for writing: writeQuery()
and writeFragment()
. These methods allow you to update the data in your local cache, to simulate an update from the server. However, beware: these updates are not actually persisted to your backend! That means if you reload your JavaScript environment the updates will be gone. Also, no other users will be able to see the changes you made with these methods. If you want all of your users to see modified data then you need to send a mutation to update it on the server.
The advantage of writeQuery()
and writeFragment()
is that they allow you to exactly modify the data in your cache to make sure it is in sync with the server in cases where you do not want to do a full server refetch. Or, in cases where you want to slightly modify some data on the client so that the user may have a better experience.
writeQuery()
has the same interface as readQuery()
, except that it also takes a named argument called data
. The data
object must be in the same shape as the JSON result your server would return for this query.
client.writeQuery({
query: gql`
{
todo(id: 1) {
completed
}
}
`,
data: {
todo: {
completed: true,
},
},
});
Likewise, writeFragment()
has the same interface as readFragment()
except for the named argument data
. The id
follows the same rules as it does in readFragment()
:
client.writeFragment({
id: '5',
fragment: gql`
fragment todo on Todo {
completed
}
`,
data: {
completed: true,
},
});
These four methods will allow you to completely control the data in your cache. You no longer have to guess at what your cache contains, as you may now simply read out any data and write back any modifications to remove inconsistencies.
Updating Data With Both Reads and Writes
We have made it easy to use these methods anywhere in your app, and especially in the context of mutation results.
Since the data you get out of the cache is a copy, you can mutate it without affecting the underlying store. This makes imperative updates simple. Here’s something you could not do before in Apollo Client:
const query = gql`
{
todos {
id
text
completed
}
}
`;
const data = client.readQuery({
query,
});
data.todos.push({
id: 5,
text: 'Hello, world!',
completed: false,
});
client.writeQuery({
query,
data,
});
Updating after a mutation
The most common place where you might want to update your store is during the lifecycle of a mutation. You often need to do this twice: first, when you immediately execute the mutation with an optimistic response (if you have one), and then a second time after your mutation has completed. Apollo Client now provides an update
method that passes in a proxy object with the four read and write methods which allows you to update your cache in whichever way you choose.
const text = 'Hello, world!';
client.mutate({
mutation: gql`
mutation ($text: String!) {
createTodo(text: $text) {
id
text
completed
}
}
`,
variables: {
text,
},
optimisticResponse: {
createTodo: {
id: -1, // Fake id
text,
completed: false,
},
},
update: (proxy, mutationResult) => {
const query = gql`
{
todos {
id
text
completed
}
}
`;
const data = proxy.readQuery({
query,
});
data.todos.push(mutationResult.createTodo);
proxy.writeQuery({
query,
data,
});
},
});
We first got the idea for using a proxy to do imperative cache updates in Greg Hurrell’s presentation on Relay 2. We then developed the idea into a full implementation which you can now use in ApolloClient
today! In Apollo Client, the main advantage of using a proxy object in update
is to apply and roll back optimistic mutation updates in a way that’s completely transparent for developers.
updateQueries Going Forward
The update
function on mutations provides a more flexible alternative to the updateQueries
callback people currently use to update their store after a mutation. There have been some issues, both in the general approach and in the specific design of updateQueries
, that led the Apollo team to keep looking for better options for updating your store after a mutation. We think that the update
function, combined with the new reading and writing methods, is a good path forward.
But we want to know what you think! Do you like updateQueries
? Do you prefer updateQueries
to update
for some use-cases? We are always interested in improving Apollo Client’s API to make it better for developers, so we depend on your feedback!
Conclusion
With the new methods, you now have complete control over the data in your cache, allowing you to use Apollo Client as a GraphQL-shaped client side database.
For more information, read the documentation for these new features.
- An introduction article to the read and write methods.
<a href="http://dev.apollodata.com/core/apollo-client-api.html#ApolloClient.readQuery" target="_blank" rel="noreferrer noopener">ApolloClient.readQuery</a>
<a href="http://dev.apollodata.com/core/apollo-client-api.html#ApolloClient.readFragment" target="_blank" rel="noreferrer noopener">ApolloClient.readFragment</a>
<a href="http://dev.apollodata.com/core/apollo-client-api.html#ApolloClient.writeQuery" target="_blank" rel="noreferrer noopener">ApolloClient.writeQuery</a>
<a href="http://dev.apollodata.com/core/apollo-client-api.html#ApolloClient.writeFragment" target="_blank" rel="noreferrer noopener">ApolloClient.writeFragment</a>