loading...
Cover image for Vue Apollo v4: the first look

Vue Apollo v4: the first look

n_tepluhina profile image Natalia Tepluhina ・9 min read

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

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.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
  }
}

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:

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">

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:

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
  }
}
// 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:

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>

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!

Discussion

pic
Editor guide
Collapse
joeschr profile image
JoeSchr

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 Author

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

Collapse
kevinnth profile image
KevinNTH

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 Author

Hi!

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

Collapse
kevinnth profile image
KevinNTH

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

Any update on this? ^^'

Collapse
kevinnth profile image
KevinNTH

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

Many thanks!

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
jiprochazka profile image
Jiří Procházka

Hi, nice work. Is there a release plan?