Local state
Outdated
This guide is outdated and needs rework for Vue 3 and vue-apollo 4. Contributions welcome!
Why use Apollo local state management?
When you perform GraphQL queries with Apollo, the results of API calls will be stored in Apollo cache. Now imagine you also need to store some kind of a local application state and make it available for different components. Usually, in Vue application we can achieve this with Vuex. But having both Apollo and Vuex will mean you store your data in two different places so you have two sources of truth.
Good thing is Apollo has a mechanism of storing local application data to cache. Previously, it used an apollo-link-state library for this. Since Apollo 2.5 release this functionality was included to Apollo core.
Creating a local schema
Just how creating a GraphQL schema is the first step toward defining our data model on the server, writing a local schema is the first step we take on the client.
Let's create a local schema to describe an item that will serve as a single element of todo-items list. This item should have some text, some property to define if it's already done or not and also an ID to distinguish one todo-item from another. So, it should be an object with three properties:
{
id: 'uniqueId',
text: 'some text',
done: false
}
Now we're ready to add an Item
type to our local GraphQL schema.
//main.js
import gql from 'graphql-tag';
export const typeDefs = gql`
type Item {
id: ID!
text: String!
done: Boolean!
}
`;
gql
here stands for the JavaScript template literal tag that parses GraphQL query strings.
Now we need to add typeDefs
to our Apollo client.
// main.js
const apolloClient = new ApolloClient({
typeDefs,
resolvers: {},
});
WARNING
As you can see, we've added also an empty resolvers
object here: if we don't assign it to the Apollo client options, it won't recognize the queries to local state and will try to send a request to remote URL instead.
Extending a remote GraphQL schema locally
You can not only create a local schema from scratch but also add a local virtual fields to your existing remote schema. These fields only exist on the client and are useful for decorating server data with local state.
Imagine we have a type User
in our remote schema:
type User {
name: String!
age: Int!
}
And we want to add a local-only property to User
:
export const schema = gql`
extend type User {
twitter: String
}
`;
Now, when querying a user, we will need to specify twitter
field is local:
const userQuery = gql`
user {
name
age
twitter @client
}
`;
Initializing an Apollo cache
To initialize an Apollo cache in your application, you will need to use an InMemoryCache
constructor. First, let's import it to your main file:
// main.js
import ApolloClient from 'apollo-boost';
import { InMemoryCache } from 'apollo-cache-inmemory';
const cache = new InMemoryCache();
Now we need to add cache to our Apollo client options:
//main.js
const apolloClient = new ApolloClient({
cache,
typeDefs,
resolvers: {},
});
Right now the cache is empty. To add some initial data to the cache, we need to use writeData
method:
// main.js
const apolloClient = new ApolloClient({
cache,
typeDefs,
resolvers: {},
});
cache.writeData({
data: {
todoItems: [
{
__typename: 'Item',
id: 'dqdBHJGgjgjg',
text: 'test',
done: true,
},
],
},
});
We've just added an array of todoItems
to our cache data and we defined that every item has a type name of Item
(specified in our local schema).
Query local data
Querying local cache is very similar to sending GraphQL queries to remote server. First, we need to create a query:
// App.vue
import gql from 'graphql-tag';
const todoItemsQuery = gql`
{
todoItems @client {
id
text
done
}
}
`;
The main difference with queries to remote API is @client
directive. This directive specifies that this query should not be executed against remote GraqhQL API. Instead, Apollo client should fetch results from the local cache.
Now, we can use this query in our Vue component as a usual Apollo query:
// App.vue
apollo: {
todoItems: {
query: todoItemsQuery
}
},
Change local data with mutations
We have two different ways to change the local data:
- direct write with
writeData
method as we did during cache initialization; - calling a GraphQL mutation.
Let's add some mutations to our local GraphQL schema:
// main.js
export const typeDefs = gql`
type Item {
id: ID!
text: String!
done: Boolean!
}
type Mutation {
checkItem(id: ID!): Boolean
addItem(text: String!): Item
}
`;
The checkItem
mutation will set the Boolean done
property of the certain item to the opposite. Let's create it using gql
:
// App.vue
const checkItemMutation = gql`
mutation($id: ID!) {
checkItem(id: $id) @client
}
`;
We defined a local mutation (because we have a @client
directive here) that will accept a unique identifier as a parameter. Now, we need a resolver: a function that resolves a value for a type or field in a schema.
In our case, resolver will define what changes do we want to make to our local Apollo cache when we have a certain mutation. Local resolvers have the same function signature as remote resolvers ((parent, args, context, info) => data
). In fact, we will need only args (arguments passed to the mutation) and context (we will need its cache property to read and write data).
Let's add a resolver to our main file:
// main.js
const resolvers = {
Mutation: {
checkItem: (_, { id }, { cache }) => {
const data = cache.readQuery({ query: todoItemsQuery });
const currentItem = data.todoItems.find(item => item.id === id);
currentItem.done = !currentItem.done;
cache.writeQuery({ query: todoItemsQuery, data });
return currentItem.done;
},
};
What are we doing here?
- read the
todoItemsQuery
from our cache to see whattodoItems
do we have now; - looking for an item with given id;
- change found item
done
property to opposite; - write our changed
todoItems
back to cache; - return the
done
property as a mutation result.
Now we need to replace an empty resolvers
object in Apollo client options with newly created resolvers
:
// main.js
const resolvers = {
Mutation: {
checkItem: (_, { id }, { cache }) => {
const data = cache.readQuery({ query: todoItemsQuery });
const currentItem = data.todoItems.find(item => item.id === id);
currentItem.done = !currentItem.done;
cache.writeQuery({ query: todoItemsQuery, data });
return currentItem.done;
},
};
const apolloClient = new ApolloClient({
cache,
typeDefs,
resolvers,
});
After this, we can use the mutation in our Vue component like normal mutations:
// App.vue
methods: {
checkItem(id) {
this.$apollo.mutate({
mutation: checkItemMutation,
variables: { id }
});
},
}