Vue Tutorial: Implement Infinite Scroll in Vue.js Using Apollo and Hasura GraphQL

23 December, 2020 | 9 min read

This articles was written by Anthony Gore as part of the Hasura Technical Writer Program. If you would like to publish an article about Hasura or GraphQL on our blog, apply here.

"Infinite Scroll" is a UI feature that allows a website to show a long list of objects to a user (posts, comments, images, etc) without overwhelming their browser or network connection.

You'll be familiar with this feature if you've used social media apps like Twitter, Reddit, or Instagram which all have an implementation of this.

A key aspect of this feature is to call data on demand. This is trivial to implement using Hasura's query pagination.

In this article, we're going to implement infinite scroll in a Vue.js app using Apollo and Hasura.

Here's a break-down of the steps we'll cover:

  • Quick set up of a Vue/Apollo app using Vue CLI
  • Set up local Hasura with Docker
  • Seed database with mock data
  • Paginated database calls with Apollo's fetchMore feature
  • Implement scroll loading in frontend

By reading this article, you'll gain a better understanding of the power of Hasura's GraphQL server and how it can be used effectively in a web app stack.

You'll also have a blueprint for implementing infinite scroll in your own JavaScript apps.

If you'd like to see the complete code, check out this GitHub repo.

Prerequisites

I assume you are familiar with the following:

  • Basics of Vue & Vue CLI
  • Basics of GraphQL

You'll need the following software on your system:

  • Docker
  • Node.js & NPM
  • Vue CLI

Creating an app with Vue CLI

To get a frontend app up and running quickly, Vue CLI is a great option. Let's create a new project by going to the terminal and typing:

$ vue create infinite-scroll

For this tutorial, you can instruct the CLI tool to create a Vue 2 app with the default settings.

Once installation completes, change into the project directory:

$ cd infinite-scroll

Adding Vue Apollo

In case you aren't familiar with Apollo, it's a GraphQL client implementation that makes it easy to bridge between a frontend app and a GraphQL server like HasuraDB.

Vue Apollo is, of course, a plugin for Vue.js that adds the Apollo client as a service that is easy to access from any Vue component.

Vue CLI makes it dead-simple to add the Vue Apollo plugin to your project:

$ vue add apollo

This will not only install the plugin but also automatically configure it!

Clearing out boilerplate files

Before we continue, let's remove some of the boilerplate provided by Vue CLI so we're ready to build our own app.

Firstly, let's delete the HelloWorld component. We'll also empty out the App.vue component, though we won't delete it completely as we'll add new content to it soon.

$ rm src/components/HelloWorld.vue
$ > src/App.vue

In a moment we're going to set up our frontend app. Before we do, let's set up our Hasura database and add some mock data to it.

Setting up a local Hasura database with Docker

As explained, we're going to get data via GraphQL from a Postgres database using Hasura.

To get up and running quickly, we're going to use Hasura in a local Docker environment. We can simply download the premade docker-compose file into our project root:

$ wget https://raw.githubusercontent.com/hasura/graphql-engine/stable/install-manifests/docker-compose/docker-compose.yaml

Then run docker-compose up -d to get it running.

If it worked, you should be able to access the Hasura console here: localhost:8080/console.

Connecting Vue Apollo to HasuraDB

For our Vue Apollo app to access the Hasura GraphQL server, there is one bit of configuration we must provide - we must create a .env file and specify the URLs as follows:

.env

VUE_APP_GRAPHQL_HTTP=http://localhost:8080/v1/graphql
VUE_APP_GRAPHQL_WS=ws://localhost:8080/v1/graphql

Data model

In this app, we're going to display a list of "posts" - think of Reddit, Twitter, or Instagram. Let's now create a table in our database to store these posts before we create some with mock data.

The structure of our posts table will be:

  • id (Integer, auto-increment)
  • url (Text)
  • title (Text)
  • created_at (Timestamp)

To create this table, go to the Hasura console. Go to the "Data" section and click on "Add Table".

Set the table name as posts. Create the columns listed above. Set the primary key to id. Finally, click "Add Table" and you'll be done.

Adding dummy data

To add some mock data for our API, let's generate it using the Mockaroo dummy data generator at https://mockaroo.com.

To use this, you need to tell it what fields you're looking for and the kind of data you want to use. Try the following:

  • id (Row Number)
  • url (Dummy Image URL)
  • title (Animal Common Name)
  • created_at (Datetime)

Now you need to tell it the kind of data you want to return. I suggest you select at least 100 rows in format JSON.

To enter this data into our table, we'll need it as a JavaScript object, but Mockaroo only gives us the JSON option.

No problem, though - we can convert JSON to JavaScript using an online conversion tool: https://www.convertonline.io/convert/json-to-js

So copy your JSON data from Mockaroo, then paste it in the JSON to JavaScript convertor.

Inserting data

To insert the data we just created into our Hasura database, we'll use the GraphQL API explorer of the console. So, navigate your browser to localhost:8080/console/api-explorer

Create this query:

mutation MyMutation {
  insert_posts(objects: []) {
    affected_rows
  }
}

Replace the array [] assigned to objects with the array of mock JavaScript objects above. Run the query and you should now have the mock data in the database.

Creating posts component

Let's return now to our Vue Apollo app. To display each post we're going to create a simple component. These components will be fed the data from the Hasura - we'll see how to do that in the next section.

First, create the component:

$ touch src/components/Post.vue

Notice that we use a prop post to supply the data, then we use the properties url and title in the template.

src/components/Post.vue

<template>
  <div class="post">
    <div>
      <img :src="post.url" :alt="post.title"/>
    </div>
    <div>
      <h3>{{ post.title }}</h3>
    </div>
  </div>
</template>
<script>
export default {
  props: ['post']
}
</script>
<style>
  .post {
    display: flex;
    flex-direction: row;
  }
</style>

Displaying posts

Next, we're going to display the posts in the app. Open up the file src/App.vue (which we emptied earlier). Add a template section where we'll iterate the post component, using the id property as a key, and binding the post object to the post prop.

In the script section, we'll import and register the Post component. Go ahead and add a post data property as well, which will be an array. You can hard-code one or more of your mock data objects from the previous step to see if it works correctly. Of course, we won't need those in a moment when we load posts from the GraphQL server.

src/App.vue

<template>
  <div id="app">
    <post v-for="post in posts" :key="post.id" :post="post" />
  </div>
</template>

<script>
import Post from '@/components/Post';

export default {
  name: 'App',
  components: { Post },
  data() {
    return {
      posts: [
        {
          id: 1
          title: 'Euro Wallaby'
          url: 'http://dummyimage.com/134x161.bmp/5fa2dd/ffffff'
        }
      ]
    }
  }
}
</script>

If you haven't already, start your Vue CLI dev server: npm run serve. Since the Hasura GraphQL server is occupying localhost port 8080, Vue CLI will start on 8081.

Navigate to localhost:8081 in your browser and you'll see our basic app skeleton.

Setting up pagination

Let's now use Apollo to make a query to grab the first "page" of our paginated posts. In the next section, we'll work on automatically grabbing the rest to properly implement the scroll load.

The first thing we'll do is create a const pageSize and set its value to 10 (or whatever integer you like). This defines how many posts are on one page. This can be defined outside the component state since it does not need to be reactive.

Next, we'll add a property page to the reactive component state. This keeps track of the current pagination page. Let's initialize to 0.

src/App.vue

...

const pageSize = 10;

export default {
  ...
  data() {
    return {
      posts: []
      page: 0
    }
  }
}

Fetching the first page of data

Now we use Apollo to query the GraphQL server and retrieve posts from the database. First, import the graphql-tag package. This would have been automatically added by Vue CLI.

Now we're going to create a query. First, access the apollo option on the component definition and assign it an object. Create a sub-property posts which tells Vue Apollo we want to use this query to add data to the posts reactive data property.

Assign an object to posts and give it a sub-property query. We'll assign to this our GraphQL query by using the gql package so we can define the query in a JavaScript string.

The query will be called GetPostPages and will have two arguments $offset and $limit. These values will be used to set the offset and limit arguments of the post object (from which we'll grab all available fields).

In Hasura, limit specifies the number of rows to include in the results offset determines where to slice the result set.

The final thing to do is declare these variables in Apollo by adding a variables sub-property to the posts object where we specify the initial values of these variables.

src/App.vue

...

import gql from 'graphql-tag';

export default {
  ...
  apollo: {
    posts: {
      query: gql`
        query GetPostPages ($offset: Int, $limit: Int) {
          posts (offset: $offset, limit: $limit, order_by: {created_at: asc}) {
            id
            url
            title,
            created_at
          }
        }
      `,
      variables: {
        offset: 0,
        limit: pageSize,
      },
    },
  }
}

Let the dev server rebuild your app and take a look at the page. Now the mock data in the Hasura database will be displayed on the page!

Note that you'll see the most current 10 posts since we ordered results by the created_at date, and we set the limit (page size) to 10.

Fetching subsequent pages

Now we want to create a method that will fetch the next results and combine them with our existing results.

First, create a new Vue method fetchMore. In this step, we'll define the method, in the next step, we'll see how to call it.

In this method, we're going to increment the page reactive value. We'll then use the fetchMore method Apollo provides to each query for pagination. This method triggers a new query and combines the results with previous results. It's perfect for use cases like pagination.

fetchMore receives a config object as an argument. We'll give it a variables property where we'll provide the updated variable values to pass to the subsequent query.

updateQuery will tell Apollo how to store the new incoming posts with the existing posts. We're simply going to merge them all into a new array.

export default {
  ...
  methods: {
    fetchMore() {
      this.page++;
      this.$apollo.queries.posts.fetchMore({
        variables: { 
          offset: (page * pageSize), 
          limit: pageSize 
        },
        updateQuery: (existing, incoming) => ({
          posts: [...existing.posts, ...incoming.fetchMoreResult.posts]
        }),
      })
    },
  },
}

Adding scroll load

In the last step, we defined a method for fetching more posts from the server. Now we want to call this anytime the user scrolls to the bottom of the page.

How do we know if the user has reached the bottom of the page? There are various browser APIs we can use for this, but it's easier to use the package scrollmonitor to do it for us.

So first, install scrollmonitor using NPM.

$ npm i scrollmonitor -S

Next, add an element with id sensor to the App component underneath the list of posts. Scroll monitor will tell us when this element is visible on the page.

If the element is visible, it means the user has scrolled to the bottom!

src/App.vue

<template>
  <div id="app">
    <post v-for="post in posts" :key="post.id" :post="post" />
    <div id="sensor"></div>
  </div>
</template>

In the App script, import scrollmonitor. We'll then use scroll monitor to set up a callback that fires any time the element enters the page. The perfect place to set this up is in the mounted hook of the component.

How it works:

  • We use a DOM query to get a reference to the sensor element
  • We create an instance of scroll monitor to watch this element
  • We can trigger a callback when the enterViewport event of scroll monitor fires. In this callback, we'll call fetchMore (note we only call it conditional on posts being defined otherwise it may call too early).

src/App.vue

...
import scrollMonitor from 'scrollmonitor';

export default {
  ...
  mounted () {
    const el = document.getElementById('sensor');
    const elementWatcher = scrollMonitor.create(el);
    elementWatcher.enterViewport(() => {
      if (this.posts)  {
        this.fetchMore();
      }
    });
  }
}

With that in place, go see your app in the browser. The infinite scroll feature means every time you reach the bottom more items appear.

This articles was written by Anthony Gore as part of the Hasura Technical Writer Program. If you would like to publish an article about Hasura or GraphQL on our blog, apply here.


About the Author

Anthony Gore provides short and sweet explanations of the latest development trends at DevNow.

Close

Get Started with GraphQL Now

Hasura Cloud gives you a fully managed, production ready GraphQL API as a service to help you build modern apps faster.

Anthony Gore

Anthony Gore

Anthony Gore provides short and sweet explanations of the latest development trends at DevNow https://devnow.buzz

Read More

Ready to get started?
Start for free on Hasura Cloud or you could contact our sales team for a detailed walk-through on how Hasura may benefit your business.
Get monthly product updates
Sign up for full access to our community highlights, new features, and occasional baby animal gifs! Oh, and we have a strict no-spam rule. ✌️