Vue Tutorial: Implement Infinite Scroll in Vue.js Using Apollo and Hasura GraphQL
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 callfetchMore
(note we only call it conditional onposts
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.