This post requires you to be already familiar with the basics of GraphQL, Apollo Client, and Vue. Shameless plug: I've tried to cover this in my talk on using Vue with Apollo. We will also use Vue Composition API. If you're not familiar with this concept, I'd highly recommend reading the Vue Composition API RFC
A few weeks ago, an alpha of version 4 of vue-apollo (the integration of Apollo client for Vue.js) was released, and I immediately decided to give it a try. What's so exciting in this version? In addition to the existing API, it has a composables option based on Vue Composition API. I've had extensive experience with vue-apollo in the past and decided to check how new API feels compared to the previous ones.
An example we're going to use
To explore the new API, I will use one of the examples already shown in my Vue+Apollo talk - I call it 'Vue Heroes'. It's a straightforward application that has one query for fetching all the heroes from the GraphQL API and two mutations: one for adding heroes and one for deleting them. The interface looks like this:
You can find the source code with the old Options API here. The GraphQL server is included; you need to run it to make the application work.
yarn apollo
Now let's start refactoring it to the new version.
Installation
As a first step, we can safely remove an old version of vue-apollo from the project:
yarn remove vue-apollo
And we need to install a new one. Starting from version 4, we can choose what API we're going to use and install the required package only. In our case, we want to try a new composables syntax:
yarn add @vue/apollo-composable
Composition API is a part of Vue 3, and it's still not released now. Luckily, we can use a standalone library to make it work with Vue 2 as well, so for now, we need to install it as well:
yarn add @vue/composition-api
Now, let's open the src/main.js
file and make some changes there. First, we need to include Composition API plugin to our Vue application:
// main.js
import VueCompositionApi from "@vue/composition-api";
Vue.use(VueCompositionApi);
We need to set up an Apollo Client using the new apollo-composable
library. Let's define a link to our GraphQL endpoint and create a cache to pass them later to the client constructor:
// main.js
import { createHttpLink } from "apollo-link-http";
import { InMemoryCache } from "apollo-cache-inmemory";
const httpLink = createHttpLink({
uri: "http://localhost:4000/graphql"
});
const cache = new InMemoryCache();
Now, we can create an Apollo Client instance:
// main.js
import { createHttpLink } from "apollo-link-http";
import { InMemoryCache } from "apollo-cache-inmemory";
import { ApolloClient } from "apollo-client";
const httpLink = createHttpLink({
uri: "http://localhost:4000/graphql"
});
const cache = new InMemoryCache();
const apolloClient = new ApolloClient({
link: httpLink,
cache
});
Creating a client wasn't really different from the previous version of Vue Apollo, and it actually has nothing to do with Vue so far - we're just setting up an Apollo Client itself. What is different is the fact we don't need to create an apolloProvider
anymore! We simply do natively provide a client to Vue application without an ApolloProvider instance:
// main.js
import { provide } from "@vue/composition-api";
import { DefaultApolloClient } from "@vue/apollo-composable";
new Vue({
setup() {
provide(DefaultApolloClient, apolloClient);
},
render: h => h(App)
}).$mount("#app");
provide
here is imported from the@vue/composition-api
package, and it enables dependency injection similar to the 2.xprovide/inject
options. The first argument inprovide
is a key, second is a value
Adding a query
To have a list of Vue heroes on the page, we need to create the allHeroes
query:
// graphql/allHeroes.query.gql
query AllHeroes {
allHeroes {
id
name
twitter
github
image
}
}
We're going to use it in our App.vue
component so let's import it there:
// App.vue
import allHeroesQuery from "./graphql/allHeroes.query.gql";
With the Options API we used this query in the Vue component apollo
property":
// App.vue
name: "app",
data() {...},
apollo: {
allHeroes: {
query: allHeroesQuery,s
}
}
Now we will modify App.vue
to make it work with Composition API. In fact, it will require to include one more option to an existing component - a setup
:
// App.vue
export default {
name: "app",
setup() {},
data() {...}
Here, within the setup
function, we will work with vue-apollo composables, and we'll need to return the results to use them in the template. Our first step is to get a result of allHeroes
query, so we need to import our first composable and pass our GraphQL query to it:
// App.vue
import allHeroesQuery from "./graphql/allHeroes.query.gql";
import { useQuery } from "@vue/apollo-composable";
export default {
name: "app",
setup() {
const { result } = useQuery(allHeroesQuery);
return { result }
},
data() {...}
useQuery
can accept up to three parameters: first is GraphQL document containing the query, second is variables object, and the third is query options. In this case, we use default options, and we don't need to pass any variables to the query, so we're passing only the first one
What is the result
here? It's exactly matching the name - it's a result of the GraphQL query, containing allHeroes
array, but it's also a reactive object - so it's a Vue ref
. That's why it wraps the resulting array in the value
property:
As Vue makes an automatic unwrap for us in the template, we can simply iterate over result.allHeroes
to render the list:
<template v-for="hero in result.allHeroes">
However, the initial value of this array is going to be undefined
because the result is still loading from the API. We can add a check here to be sure we already have a result like result && result.allHeroes
but v4 has a useful helper to do this for us - useResult
. It's a great utility to help you shaping the result you fetched from the API, especially useful if you need to get some deeply nested data or a few different results from one query:
<template v-for="hero in allHeroes">
<script>
import { useQuery, useResult } from "@vue/apollo-composable";
export default {
setup() {
const { result } = useQuery(allHeroesQuery);
const allHeroes = useResult(result, null, data => data.allHeroes)
return { allHeroes }
},
}
</script>
useResult
takes three parameters: the result of GraphQL query, a default value (null
in our case), and a picking function that returns data we want to retrieve from the result object. If the result contains the only one property (like allHeroes
in our case), we can simplify it a bit:
// App.vue
setup() {
const { result } = useQuery(allHeroesQuery);
const allHeroes = useResult(result)
return { allHeroes }
},
The only thing left is to display a loading status when we're actually fetching the data from he API. Aside from the result
, useQuery
can return a loading
as well:
// App.vue
setup() {
const { result, loading } = useQuery(allHeroesQuery);
const allHeroes = useResult(result)
return { allHeroes, loading }
},
And we can render it conditionally in out template:
<h2 v-if="loading">Loading...</h2>
Let's compare the code we had for v3 with the new one:
While the new syntax is more verbose, it's also more customizable (to shape the response, we would need to add an update
property to v3 syntax). I like we can expose loading
properly for every single query instead of using it as a nested property of the global $apollo
object.
Working with mutations
Now let's also refactor mutations we have to the new syntax as well. In this application, we have two mutations: one to add a new hero and one to delete an existing hero:
// graphql/addHero.mutation.gql
mutation AddHero($hero: HeroInput!) {
addHero(hero: $hero) {
id
twitter
name
github
image
}
}
// graphql/deleteHero.mutation.gql
mutation DeleteHero($name: String!) {
deleteHero(name: $name)
}
In the Options API syntax, we were calling mutation as a method of the Vue instance $apollo
property:
this.$apollo.mutate({
mutation: mutationName,
})
Let's start refactoring with the addHero
one. Similarly to query, we need to import the mutation to the App.vue
and pass it as a parameter to useMutation
composable function:
// App.vue
import addHeroMutation from "./graphql/addHero.mutation.gql";
import { useQuery, useResult, useMutation } from "@vue/apollo-composable";
export default {
setup() {
const { result, loading } = useQuery(allHeroesQuery);
const allHeroes = useResult(result)
const { mutate } = useMutation(addHeroMutation)
},
}
The mutate
here is actually a method we need to call to send the mutation to our GraphQL API endpoint. However, in the case of addHero
mutation, we also need to send a variable hero
to define the hero we want to add to our list. The good thing is that we can return this method from the setup
function and use it within the Options API method. Let's also rename the mutate
function as we'll have 2 mutations, so giving it a more intuitive name is a good idea:
// App.vue
setup() {
const { result, loading } = useQuery(allHeroesQuery);
const allHeroes = useResult(result)
const { mutate: addNewHero } = useMutation(addHeroMutation)
return { allHeroes, loading, addNewHero }
},
Now we can call it in the addHero
method already present in the component:
export default {
setup() {...},
methods: {
addHero() {
const hero = {
name: this.name,
image: this.image,
twitter: this.twitter,
github: this.github,
github: this.github
};
this.addNewHero({ hero });
}
}
}
As you can see, we passed a variable at the moment mutation is called. There is an alternative way, we can also add variables to the options object and pass it to the useMutation
function as a second parameter:
const { mutate: addNewHero } = useMutation(addHeroMutation, {
variables: {
hero: someHero
}
})
Now our mutation will be successfully sent to the GraphQL server. Still, we also need to update the local Apollo cache on a successful response - otherwise, the list of heroes won't change until we reload the page. So, we also need to read the allHeroes
query from Apollo cache, change the list adding a new hero and write it back. We will do this within the update
function (we can pass it with the options
parameter as we can do with variables
):
// App.vue
setup() {
const { result, loading } = useQuery(allHeroesQuery);
const allHeroes = useResult(result)
const { mutate: addNewHero } = useMutation(addHeroMutation, {
update: (cache, { data: { addHero } }) => {
const data = cache.readQuery({ query: allHeroesQuery });
data.allHeroes = [...data.allHeroes, addHero];
cache.writeQuery({ query: allHeroesQuery, data });
}
})
return { allHeroes, loading, addNewHero }
},
Now, what's about loading state when we're adding a new hero? With v3 it was implemented with creating an external flag and changing it on finally
:
// App.vue
export default {
data() {
return {
isSaving: false
};
},
methods: {
addHero() {
...
this.isSaving = true;
this.$apollo
.mutate({
mutation: addHeroMutation,
variables: {
hero
},
update: (store, { data: { addHero } }) => {
const data = store.readQuery({ query: allHeroesQuery });
data.allHeroes.push(addHero);
store.writeQuery({ query: allHeroesQuery, data });
}
})
.finally(() => {
this.isSaving = false;
});
}
}
}
In v4 composition API we can simply return the loading state for a given mutation from the useMutation
function:
setup() {
...
const { mutate: addNewHero, loading: isSaving } = useMutation(
addHeroMutation,
{
update: (cache, { data: { addHero } }) => {
const data = cache.readQuery({ query: allHeroesQuery });
data.allHeroes = [...data.allHeroes, addHero];
cache.writeQuery({ query: allHeroesQuery, data });
}
}
);
return {
...
addNewHero,
isSaving
};
}
Let's compare the code we had for v3 with v4 composition API:
In my opinion, the composition API code became more structured, and it also doesn't require an external flag to keep the loading state.
deleteHero
mutation could be refactored in a really similar way except one important point: in update
function we need to delete a hero found by name and the name is only available in the template (because we're iterating through the heroes list with v-for
directive and we can't get hero.name
outside of the v-for
loop). That's why we need to pass an update
function in the options parameter directly where the mutation is called:
<vue-hero
v-for="hero in allHeroes"
:hero="hero"
@deleteHero="
deleteHero(
{ name: $event },
{
update: cache => updateHeroAfterDelete(cache, $event)
}
)
"
:key="hero.name"
></vue-hero>
<script>
export default {
setup() {
...
const { mutate: deleteHero } = useMutation(deleteHeroMutation);
const updateHeroAfterDelete = (cache, name) => {
const data = cache.readQuery({ query: allHeroesQuery });
data.allHeroes = data.allHeroes.filter(hero => hero.name !== name);
cache.writeQuery({ query: allHeroesQuery, data });
};
return {
...
deleteHero,
updateHeroAfterDelete,
};
}
}
</script>
Conclusions
I really like the code abstraction level provided with vue-apollo v4 composables. Without creating a provider
and injecting an $apollo
object to Vue instance, there will be easier to mock Apollo client in unit tests. The code also feels more structured and straightforward to me. I will be waiting for the release to try in on the real-world projects!
Top comments (11)
Hey Natalia,
I've just seen your talk regarding bit.ly/apollo-is-love! You said that using apollo-composable is easier to mock Apollo client in unit tests, may I ask some examples to show us please?
It would be nice to implement testing in this repository bit.ly/apollo-is-love too :)
I'm looking forward to having your answer, take care.
Hi!
Ok, will try to add some examples there next week! :)
Hey Natalia, I hope that your are doing fine :)
Any update on this? ^^'
I'd love to! Please ping me then :)
Many thanks!
Loved your talk, thanks for bearing through it with your cold, hope you are well again!
And thanks for writing it together here.
If anybody else is searching for the github repo, I believe it's this: github.com/NataliaTepluhina/vue-gr...Scratch that, it's given at the end of the talk and is here and more uptodate: bit.ly/apollo-is-love
Thank you for reading it and for listening to my talk! So happy you liked it :)
Hi, thanks for sharing your works! It's a great helping material. Maybe you can help me with a question about How can we do the filtering item's implementation with useQuery. If I have a list of items that I get with a first "useQuery" and then I want filtering by category name for example in a second "useQuery", In your example could be filtering people by names... how can we do that inplementation in composition api. Do I need to update too? Thanks again!
Hi Natalia, like all your talks and blogposts especially on this topic! Thanks for advocating this direction! I started to elaborate on this solution because it has great potential but stumbled upon an issue. The provide in the setup of main is resulting in blank page without errors in Internet explorer. Are you aware of this issue and do you have a solution for this?
Hi Natalia, Hi, I'm in the process of migrating a project from Vue 2.7 to 3.4 and that also includes migrating from Vue Apollo 3 to 4.
It's proving to be such a massive problem I'm giving serious thought to abandoning GraphQL and returning to REST, which is the last thing I want to do.
I've spent the last couple of week attempting to find some clue as to how (assuming it's possible) to use Vue Apollo 4 in a composable, same as how I've been using Vue Apollo 3 in mixins.
I'd be keen to know what you think!
Hi Natalia! Great work of yours, i'm learning Vue and Apollo at the same time and i'm having some torubles like "Use provideApolloClient() if you are outside of a component setup.".
Anyway, that's not the point. I was wondering if you could share the the repo after the refactor you are showing here, maybe i'm missing something.
Thank's!
Hi, nice work. Is there a release plan?