DEV Community

Cover image for Vue Apollo v4: the first look
Natalia Tepluhina
Natalia Tepluhina

Posted on

Vue Apollo v4: the first look

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:

Application view

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

provide here is imported from the @vue/composition-api package, and it enables dependency injection similar to the 2.x provide/inject options. The first argument in provide is a key, second is a value

3.x 4.x composables syntax
3.x 4.x

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

With the Options API we used this query in the Vue component apollo property":

// App.vue

  name: "app",
  data() {...},
  apollo: {
    allHeroes: {
      query: allHeroesQuery,s
    }
  }
Enter fullscreen mode Exit fullscreen mode

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() {...}
Enter fullscreen mode Exit fullscreen mode

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() {...}
Enter fullscreen mode Exit fullscreen mode

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:

Result structure

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">
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 }
},
Enter fullscreen mode Exit fullscreen mode

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 }
},
Enter fullscreen mode Exit fullscreen mode

And we can render it conditionally in out template:

<h2 v-if="loading">Loading...</h2>
Enter fullscreen mode Exit fullscreen mode

Let's compare the code we had for v3 with the new one:

3.x 4.x composables syntax
3.x 4.x

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
  }
}
Enter fullscreen mode Exit fullscreen mode
// graphql/deleteHero.mutation.gql

mutation DeleteHero($name: String!) {
  deleteHero(name: $name)
}
Enter fullscreen mode Exit fullscreen mode

In the Options API syntax, we were calling mutation as a method of the Vue instance $apollo property:

this.$apollo.mutate({
  mutation: mutationName,
})
Enter fullscreen mode Exit fullscreen mode

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)
  },
}
Enter fullscreen mode Exit fullscreen mode

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 }
},
Enter fullscreen mode Exit fullscreen mode

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 });
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
  }
})
Enter fullscreen mode Exit fullscreen mode

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 }
},
Enter fullscreen mode Exit fullscreen mode

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;
        });
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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
  };
}
Enter fullscreen mode Exit fullscreen mode

Let's compare the code we had for v3 with v4 composition API:

3.x 4.x composables syntax
3.x 4.x

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>
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
knth profile image
KNTH

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.

Collapse
 
n_tepluhina profile image
Natalia Tepluhina

Hi!

Ok, will try to add some examples there next week! :)

Collapse
 
knth profile image
KNTH

Hey Natalia, I hope that your are doing fine :)

Any update on this? ^^'

Collapse
 
knth profile image
KNTH

I'd love to! Please ping me then :)

Many thanks!

Collapse
 
joeschr profile image
JoeSchr • Edited

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

Collapse
 
n_tepluhina profile image
Natalia Tepluhina

Thank you for reading it and for listening to my talk! So happy you liked it :)

Collapse
 
jcrobles76 profile image
jcrobles76

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!

Collapse
 
frnsz profile image
Fransz

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?

Collapse
 
octaneinteractive profile image
Wayne Smallman • Edited

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!

Collapse
 
jaymc profile image
Juan Carabetta

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!

Collapse
 
jiprochazka profile image
Jiří Procházka

Hi, nice work. Is there a release plan?