Chapter 07 - Mastering Vue.js and GraphQL: Build Dynamic Web Apps with Apollo Client

Vue.js and GraphQL integration using Apollo Client. Seamless data fetching with queries, mutations, and subscriptions. Best practices include separating operations, using fragments, caching, error handling, pagination, and testing with mock Apollo Client.

Chapter 07 - Mastering Vue.js and GraphQL: Build Dynamic Web Apps with Apollo Client

Vue.js and GraphQL make a powerful duo for building modern web applications. Let’s dive into how we can integrate these technologies using Apollo Client to create a seamless data fetching experience.

First things first, we need to set up our Vue.js project and install the necessary dependencies. If you haven’t already, create a new Vue project using the Vue CLI:

vue create vue-graphql-demo
cd vue-graphql-demo

Now, let’s install Apollo Client and the Vue Apollo integration:

npm install @apollo/client graphql vue-apollo

With our dependencies in place, it’s time to configure Apollo Client in our Vue application. Open your main.js file and add the following:

import { createApp, h } from 'vue'
import { ApolloClient, createHttpLink, InMemoryCache } from '@apollo/client/core'
import { DefaultApolloClient } from '@vue/apollo-composable'
import App from './App.vue'

const httpLink = createHttpLink({
  uri: 'https://your-graphql-endpoint.com/graphql',
})

const cache = new InMemoryCache()

const apolloClient = new ApolloClient({
  link: httpLink,
  cache,
})

const app = createApp({
  setup() {
    provide(DefaultApolloClient, apolloClient)
  },
  render: () => h(App),
})

app.mount('#app')

This code sets up Apollo Client with a basic configuration, pointing to your GraphQL endpoint. Make sure to replace the URI with your actual GraphQL server address.

Now that we’ve got Apollo Client set up, let’s explore how to perform queries, mutations, and subscriptions in our Vue components.

Let’s start with a simple query. Imagine we have a GraphQL server that provides a list of books. We’ll create a component to display this list:

<template>
  <div>
    <h2>Book List</h2>
    <ul v-if="result.data">
      <li v-for="book in result.data.books" :key="book.id">
        {{ book.title }} by {{ book.author }}
      </li>
    </ul>
    <p v-else-if="result.error">Error: {{ result.error.message }}</p>
    <p v-else>Loading...</p>
  </div>
</template>

<script>
import { useQuery } from '@vue/apollo-composable'
import gql from 'graphql-tag'

const GET_BOOKS = gql`
  query GetBooks {
    books {
      id
      title
      author
    }
  }
`

export default {
  setup() {
    const { result } = useQuery(GET_BOOKS)
    return { result }
  }
}
</script>

In this component, we’re using the useQuery composition function from Vue Apollo to fetch our book data. The query is defined using the gql tag, and the result is automatically reactive. We’re displaying a loading message while the data is being fetched, an error message if something goes wrong, and the list of books once the data is available.

Now, let’s look at how we can perform a mutation to add a new book to our list:

<template>
  <div>
    <h2>Add New Book</h2>
    <form @submit.prevent="addBook">
      <input v-model="title" placeholder="Title" required>
      <input v-model="author" placeholder="Author" required>
      <button type="submit">Add Book</button>
    </form>
  </div>
</template>

<script>
import { ref } from 'vue'
import { useMutation } from '@vue/apollo-composable'
import gql from 'graphql-tag'

const ADD_BOOK = gql`
  mutation AddBook($title: String!, $author: String!) {
    addBook(title: $title, author: $author) {
      id
      title
      author
    }
  }
`

export default {
  setup() {
    const title = ref('')
    const author = ref('')

    const { mutate: addBook, onDone } = useMutation(ADD_BOOK)

    const handleAddBook = () => {
      addBook({ title: title.value, author: author.value })
    }

    onDone(() => {
      title.value = ''
      author.value = ''
    })

    return {
      title,
      author,
      addBook: handleAddBook
    }
  }
}
</script>

In this component, we’re using the useMutation composition function to define our mutation. We’ve created a form that allows users to input a new book’s title and author. When the form is submitted, the addBook mutation is called with the input values. After the mutation is complete, we clear the input fields.

Lastly, let’s look at how we can implement a subscription to get real-time updates when new books are added:

<template>
  <div>
    <h2>New Books</h2>
    <ul>
      <li v-for="book in newBooks" :key="book.id">
        {{ book.title }} by {{ book.author }}
      </li>
    </ul>
  </div>
</template>

<script>
import { ref } from 'vue'
import { useSubscription } from '@vue/apollo-composable'
import gql from 'graphql-tag'

const BOOK_ADDED_SUBSCRIPTION = gql`
  subscription BookAdded {
    bookAdded {
      id
      title
      author
    }
  }
`

export default {
  setup() {
    const newBooks = ref([])

    useSubscription(BOOK_ADDED_SUBSCRIPTION, null, (_, response) => {
      newBooks.value.push(response.data.bookAdded)
    })

    return { newBooks }
  }
}
</script>

In this component, we’re using the useSubscription composition function to listen for new books being added. Whenever a new book is added, it’s pushed to our newBooks array, which is then rendered in the template.

Now, let’s talk about some best practices when working with Vue.js and GraphQL. First, it’s a good idea to separate your GraphQL operations into their own files. This keeps your components cleaner and allows for easier reuse of queries and mutations across your application.

For example, you might have a graphql folder in your project with files like queries.js, mutations.js, and subscriptions.js:

// queries.js
import gql from 'graphql-tag'

export const GET_BOOKS = gql`
  query GetBooks {
    books {
      id
      title
      author
    }
  }
`

// mutations.js
import gql from 'graphql-tag'

export const ADD_BOOK = gql`
  mutation AddBook($title: String!, $author: String!) {
    addBook(title: $title, author: $author) {
      id
      title
      author
    }
  }
`

// subscriptions.js
import gql from 'graphql-tag'

export const BOOK_ADDED_SUBSCRIPTION = gql`
  subscription BookAdded {
    bookAdded {
      id
      title
      author
    }
  }
`

Then, in your components, you can import these operations as needed:

import { GET_BOOKS } from '@/graphql/queries'
import { ADD_BOOK } from '@/graphql/mutations'
import { BOOK_ADDED_SUBSCRIPTION } from '@/graphql/subscriptions'

Another best practice is to use fragments to share fields between queries and mutations. This can help reduce duplication and make your GraphQL operations more maintainable. For example:

const BOOK_FRAGMENT = gql`
  fragment BookFields on Book {
    id
    title
    author
  }
`

export const GET_BOOKS = gql`
  query GetBooks {
    books {
      ...BookFields
    }
  }
  ${BOOK_FRAGMENT}
`

export const ADD_BOOK = gql`
  mutation AddBook($title: String!, $author: String!) {
    addBook(title: $title, author: $author) {
      ...BookFields
    }
  }
  ${BOOK_FRAGMENT}
`

When working with Apollo Client, it’s also important to understand caching. By default, Apollo Client caches query results, which can improve performance by reducing unnecessary network requests. However, you may need to manually update the cache after mutations to ensure your UI stays in sync with your server data.

For example, after adding a new book, you might want to update the cache to include this new book in the list of all books:

const { mutate: addBook } = useMutation(ADD_BOOK, {
  update: (cache, { data: { addBook } }) => {
    const data = cache.readQuery({ query: GET_BOOKS })
    cache.writeQuery({
      query: GET_BOOKS,
      data: {
        books: [...data.books, addBook]
      }
    })
  }
})

This code reads the current list of books from the cache, adds the new book to that list, and then writes the updated list back to the cache.

Error handling is another crucial aspect of working with GraphQL in Vue.js. Apollo Client provides error objects that you can use to display meaningful error messages to your users. For example:

<template>
  <div>
    <p v-if="error">Error: {{ error.message }}</p>
    <!-- Rest of your component -->
  </div>
</template>

<script>
import { useQuery } from '@vue/apollo-composable'
import { GET_BOOKS } from '@/graphql/queries'

export default {
  setup() {
    const { result, error, loading } = useQuery(GET_BOOKS)
    return { result, error, loading }
  }
}
</script>

In this example, we’re destructuring the error object from useQuery and displaying its message if an error occurs.

When it comes to performance, one technique you can use is pagination. Instead of fetching all books at once, you might want to fetch them in smaller chunks. GraphQL makes this easy with its built-in pagination support:

const GET_BOOKS = gql`
  query GetBooks($offset: Int!, $limit: Int!) {
    books(offset: $offset, limit: $limit) {
      id
      title
      author
    }
  }
`

// In your component
const { result, fetchMore } = useQuery(GET_BOOKS, {
  offset: 0,
  limit: 10
})

const loadMore = () => {
  fetchMore({
    variables: {
      offset: result.value.books.length,
      limit: 10
    },
    updateQuery: (previousResult, { fetchMoreResult }) => {
      return {
        books: [...previousResult.books, ...fetchMoreResult.books]
      }
    }
  })
}

This code fetches the first 10 books initially, and then allows you to load more books as needed.

Testing is an essential part of any robust application. When testing components that use GraphQL queries or mutations, you’ll want to mock the Apollo Client to avoid making actual network requests. Here’s a simple example using Jest:

import { mount } from '@vue/test-utils'
import { createMockClient } from 'mock-apollo-client'
import { DefaultApolloClient } from '@vue/apollo-composable'
import BookList from '@/components/BookList.vue'
import { GET_BOOKS } from '@/graphql/queries'

describe('BookList', () => {
  it('renders books when loaded', async () => {
    const mockClient = createMockClient()
    mockClient.setRequestHandler(GET_BOOKS, () => Promise.resolve({
      data: {
        books: [
          { id: '1', title: 'Test Book', author: 'Test Author' }
        ]
      }
    }))

    const wrapper = mount(BookList, {
      global: {
        provide: {
          [DefaultApolloClient]: mockClient
        }
      }
    })

    await wrapper