It's hard to think of a web or mobile application in 2019 that doesn't have at least one feed or list component. Whether it's for a news feed, search results page, or tabbed-out list of resources, different methods of pagination are constantly being used. One of the most popular pagination experiences is the infamously addictive Infinite Scroll.
Before we jump into building an infinite scroll component, let's answer one question. Why is infinite scroll – or pagination in general – useful?
Imagine that you're working on a popular news app. There are 10,000's of articles in the archives and dozens being published every day. The news feed in your app sorts articles by publishing date so that the newest appear first. However, sorted or not, if the feed loads ALL articles every time a user opens their app, the infinite scroll feed would instead be an infinite load feed and everyone would be sad.
This is where pagination, in its various forms, comes to save the day. Instead of your feed loading ALL of the news, it's able to quickly request – for example – the 25 most recent articles. Then when the user requests more news the feed will fetch articles 26 through 50 and so on. This makes sure that response times are quick and there is never too much data being needlessly transferred.
Rather just play with the code yourself? it's live on CodeSandbox! Feel free to check it out.
Also... here's the GitHub repo
Getting Started
So with all of that babel in mind, let's actually build an infinite scroll component using VueJS and a GraphQL API. You'll likely be able to re-use the final component in any of your VueJS projects, so think of it as a new tool in your toolbox once done!
Prerequisites
This tutorial assumes that you are:
- Somewhat familiar with Vue
- Have Node installed on your computer
- Use npm or yarn
- You are excited about playing with GraphQL!
Using Vue instant prototyping
The most under-valued and epic (in my humble opinion) Vue feature is instant prototyping. What does that mean? It means that without having to set up an entire Vue project you can develop a single component in isolation.
This will let us (almost) immediately jump into developing our component, so let's install it during our short set up section.
Installing the CLI and dependences
First, we're going to install the Vue CLI and an extra extension package that gives us the ability to use instant prototyping.
# Install Vue CLI
npm install -g @vue/cli
# Install the CLI instant prototyping
npm install -g @vue/cli-service-global
Next, create a new directory on your computer and call it whatever you'd like. Here we'll write all the code for our component.
# Create new directory
mkdir infinite-scroll-component
# Change into new directory
cd infinite-scroll-component
Now, we're going to create our component files and an examples
directory. Why? Because when developing our component we'll want to import/interact with it like we would in a full application. Thus, the examples
directory will allow us to do just that by requiring our infinite scroll component as a local dependency.
# This will create the examples directory and all required files
mkdir examples && touch index.js infiniteScroll.vue examples/default.vue
# Your directory should look like this
tree .
=>
infinite-scroll-component
├── examples
│ └── default.vue
├── index.js
└── infiniteScroll.vue
Finally, we're going to want to initialize a new NPM project in both the root and examples directory. When doing this, just accept all the defaults before installing the project's dependencies.
# Init new npm project in ROOT directory
npm init
# Install dependencies
npm install --save vue graphql-request
# Change into examples directory and init new npm project
cd examples && npm init
# Require the infinite scroll component as a local dependency!
npm install --save ..
Voila! If only setup was always that easy...
With instant prototyping we can now run vue serve FILE_NAME
and it will spin up the development server for that single file. Try it! You will probably be... potentially underwhelmed since our files are still empty 🙃
That said before we start writing our Vue components we're going to set up the GraphQL API. Why? Because it is SO MUCH MORE FUN developing when there's data. Hopefully, you agree!
Setting up a GraphQL API on 8base
There are many different ways to accomplish setting up a GraphQL server and API. However, we'll use 8base so that everything is extremely quick to set up and super stable. To get started, we'll only need to take the following few steps.
1) Sign Up
If you have an existing account, visit your 8base Dashboard and select an existing Workspace. If you don’t have an account, create one on 8base. Their free plan will work for what we need.
2) Building the Data Model
In the workspace, navigate to the Data Builder page and click on “+ Add Table” to start building the data model. Were going to create just one called Articles
with the following fields.
Articles
Field | Type | Description | Options |
---|---|---|---|
title |
Text | Article title | mandatory=True |
body |
Text | Article body |
mandatory=True , characters=1000
|
3) Adding Dummy Data
Let's add some dummy records to our database. I've uploaded a DummyData.csv file. Save it and then open the Data
tab that's right next to the Schema
tab in the 8base Data Builder.
On the far right of the Data Viewer there is a dropdown with an Import CSV option. Select the DummyData.csv
file from your downloads folder and make sure to specify "Has Header Row" in the modal that appears. You might need to map the column names to the appropriate table fields. However, once done, the import should only take a few seconds.
4) Roles and Permissions
To allow our app to securely access the 8base GraphQL API with appropriate permissions were going to create an API Token with a custom role attached. Navigate to Settings > Roles
and create a new role with the name "FeedAppClientRole". Once created, click the role to update its permissions.
Here we can update what permissions the FeedAppClientRole is allowed. In our case, we ONLY want it to be able to query/read articles. Let's check/uncheck the appropriate boxes to enforce that.
FeedAppClientRole Permissions
Table | Create | Read | Update | Delete | Fields |
---|---|---|---|---|---|
Articles | False | All Records | No Records | False | Full Access |
Now we need to attach this role to an API Token that can be added to our app. Navigate to Settings > API Tokens
and add a new token by giving it a name and selecting under "Roles" the FeedAppClientRole that we just created.
Make sure to copy the API token once created! You won't be able to view it again.
5) Getting the Workspace API Endpoint
Finally, let’s copy our workspace’s API endpoint. This endpoint is unique to our workspace and is where we will send all of our GraphQL queries for new records.
There are a few ways to obtain the endpoint. However, just navigate to the workspace Home page and you’ll find the endpoint in the bottom left.
6) Testing that it works!
We should probably test that our API is properly set up before we keep going. How, you might ask? By querying it! Instead of setting up or using some GraphQL client, let's just run a good 'ol fashion curl command in our terminal and view the response.
Make sure to replace <YOUR_API_ENDPOINT>
with your workspace API endpoint and <YOUR_API_TOKEN>
with the API Token you created.
curl -X POST '<YOUR_API_ENDPOINT>' \
-H "Content-Type: application/json" \
-H 'Authorization: Bearer <YOUR_API_TOKEN>' \
-d '{ "query": "{ articlesList(first: 10) { items { title } } }"}'
Does the JSON response show a list of article titles? Woo hoo! Nice work. We're now ready to keep cruizing and move into the creating the component.
Setting up the component
So, let's quickly list out what our infinite scroll component is going to have to do. That way we'll be able to think more clearly about the steps we need to take.
Simple Spec
- Query a GraphQL endpoint for N many records.
- Allow the user to scroll vertically through a rendered list.
- Recognize when the user has reached the end of the list.
- Query N additional records and append them to the list.
- Allow for the developer to specify a template for list items.
With these bullets in mind, let's add some code to our files so that we have a structure to work with.
examples/default.vue
Again, the reason that we have the examples/default.vue file is so we can import the component being developed like we would in a full application. Go ahead and run vue serve examples/default.vue
– or vue serve default.vue
, if you're already in the examples directory. This will spin up the instant prototyping development server. You may see some errors while making incremental file updates; just ignore them for now.
Per our simple spec, we want an infinite scroll component that fetches a specified number of records from a GraphQL API. Additionally, we want to be able to specify a template that will get rendered for each record that's fetched.
With that in mind, let's create an example of how we'd like to use our component. Always read the in-code comments!
<style scoped>
* {
font-family: Arial, Helvetica, sans-serif;
}
.container {
margin: 0 auto;
width: 420px;
}
</style>
<template>
<!--
Here's our InfiniteScroll component. We want to pass it some simple props so that the component knows...
1) query: The GraphQL query to run.
2) limit: How many records to fetch.
3) respKey: A key for accessing the response.
4) endpoint: Where to fetch the records from.
5) authToken: If needed, a token to access the API.
-->
<section class="container">
<InfiniteScroll
:query="query"
:limit="limit"
:respKey="respKey"
:endpoint="endpoint"
:authToken="authToken">
<!--
Instead of being stuck with a generic template, we want to be able to render out each record that gets fetched with a
custom template.
1) Using v-slot we can name the scoped data that's passed to the template.
2) The template is a child component of InfiniteScrollm so we can access it using <slot />
-->
<template v-slot="item">
<!--
Using the scoped slot data, we're creating a simple template that will render out the wanted data from our fetched records.
-->
<article>
<h4>{{ item.title }}</h4>
<p>{{ item.body }}</p>
</article>
</template>
</InfiniteScroll>
</section>
</template>
<!--
Next up... <script> will go here
-->
Sweet! We essentially just typed out how we'd like to use our InfiniteScroll
component. It looks pretty intuitive, right? Now we have to ACTUALLY BUILD IT... Before that though, let's add the <script>
tag to our examples/default.vue
file so that all the named data values are present.
Just place the following code right below the <template>
tag and the comments!
<script>
/**
* We've got to import our infinite scroll component!
*/
import { InfiniteScroll } from 'infinite-scroll-component';
export default {
/**
* Registering the component will allow us to
* use it in our template, as is shown above.
*/
components: {
InfiniteScroll
},
data() {
return {
/**
* Here we've adding the values to that are
* getting passed to the InfiniteScroll
* comonent. They could be directly added in,
* the template, though are better organized
* in the data method like this.
*/
limit: 25,
/**
* Depending on the API, the returned records may be
* nested in the response object. This key is optional
* though can be used for accessing a nested list.
*/
respKey: 'articlesList.items',
/**
* Optional auth token, depending on GraphQL API
* REPLACE IT WITH YOUR API TOKEN
*/
authToken: 'YOUR API TOKEN',
/**
* Required GraphQL API endpoint from where records get fetched.
* REPLACE IT WITH YOUR WORKSPACE API ENDPOINT
*/
endpoint: 'YOUR WORKSPACE API ENDPOINT',
/**
* Required GraphQL query for fetching records. This query
* is designed for our 8base API and will return the paginated
* results from our articles table.
*/
query: `query($limit: Int, $skip: Int) {
articlesList(first: $limit, skip: $skip) {
items {
id
title
body
}
}
}`
}
}
};
</script>
Nice work! Our examples/default.vue
component is set up. Make sure that you updated the endpoint
and apiToken
values with those from your workspace.
infiniteScroll.vue
Now it's time for the actual InfiniteScroll
component. Similarly to the last component, let's start with the <template>
and <style>
tags. Infinite scrolling does require some functional styling.
<style scoped>
/**
* Some of the styling is functional, while other
* is aesthetic. Feel free to play with it!
*/
section {
overflow-y: scroll;
height: 500px;
}
</style>
<template>
<!--
Component container with scroll event listener
for triggering handle scroll event.
-->
<section @scroll="handleScroll">
<!--
For every item in the items array, render
the slotted template and bind the item data.
-->
<slot v-for="item in items" v-bind="item" />
</section>
</template>
<!--
Next up... <script> will go here
-->
I know. It's almost frustratingly simple, right? However, why make something more complex than it has to be? All we want to do is template every record fetched from our API, as well as know when to fetch more of them. That is what this does.
So, let's now add the <script>
tag that will make everything actually work.
<script>
/* eslint-disable no-console */
/* Imports the graphQL request client */
import { GraphQLClient } from "graphql-request";
export default {
/**
* Declare the props expected to be passed from
* any parent component (the ones in Dev.vue).
*/
props: {
query: {
type: String,
required: true
},
respKey: {
type: String,
default: ""
},
limit: {
type: Number,
default: 25
},
endpoint: {
type: String,
required: true
},
authToken: {
type: String,
default: ""
}
},
data() {
return {
/* The array for storing all records fetched */
items: [],
/**
* Configure the GraphQL Client, setting headers
* only if the authTokenis specified.
*/
client: new GraphQLClient(
this.endpoint,
this.authToken
? {
headers: {
authorization: `Bearer ${this.authToken}`
}
}
: null
)
};
},
computed: {
respKeyParser() {
return this.respKey.split('.')
}
},
methods: {
/**
* Callback for the onscroll event checks whether the scroll position
* is near the bottom of the scroll container.
*/
handleScroll({ target: { scrollTop, clientHeight, scrollHeight } }) {
if (scrollTop + clientHeight >= scrollHeight) this.loadBatch();
},
/**
* When a new batch of articles are retrieved from the API,
* add them to the items.
*/
handleLoad(response) {
if (this.respKey) {
response = this.respKeyParser.reduce((o, v) => o[v], response)
}
this.items = this.items.concat(response);
},
/**
* Use the client to send query to GraphQL API
* with the needed variables, 'limit' and 'skip'.
*/
loadBatch() {
this.client
.request(this.query, {
limit: this.limit,
skip: this.items.length
})
.then(this.handleLoad)
.catch(console.error);
}
},
/**
* When the component mounts (first renders), load the
* initial batch of posts.
*/
mounted() {
this.loadBatch();
}
};
</script>
Naturally, this part is a little bit meatier than the others. That said, there are really only several things worth pointing out; the in-code documentation should handle the rest.
First off, we initialize the GraphQLClient
and conditionally pass it headers
depending on whether or not an authToken
was passed. The initialized client is what gets used in the loadBatch
method to execute GraphQL calls to our API. It uses the required query
prop, which receives the skip
and limit
variables.
The skip
and limit
variables are what the articlesList
query requires to handle pagination. While limit
simply represents how many records to load per request, skip
specifies how many records have already been loaded – or from which index in the list to slice. Thus, if we initially fetch records A
, B
, and C
from our API with limit = 3, skip = 0
, and then on the following request specify limit = 3, skip = 3
, we'll receive records D
, E
, and F
.
Lastly, let's look at the handleScroll
method. This is that callback method for the @scroll
event. By unpacking the passed event
argument we get access to the scrollTop
, clientHeight
, and scrollHeight
values. The clientHeight
is a fixed value that represents the height of the scrollable element in pixels. Meanwhile, scrollTop
is changing on every scroll event to represent the distance from the top of the scroll container to the current position.
If the clientHeight
plus scrollTop
is greater than to or equal to the scrollHeight
(the scrollable height of the element in pixels) then we know the container has been fully scrolled!
index.js
Wondering why your component isn't appearing in the browser (http://localhost:8080)? We didn't export it!
Update the index.js
file with the following:
import InfiniteScroll from './infiniteScroll.vue';
export { InfiniteScroll }
Wrap up and some other fun stuff
Our pagination component is done! It can now be used with any Vue project with any GraphQL API. The component should be rendering out the records in the browser. If not, check out the errors and let me know if something is acting funky!
Additionally, if you're interested in building a full pagination component (tabs with navigation) as opposed to an infinite scroll. Check out this Pagination in Vue.js article by Denny Hendrick.
With that said, here is the tutorial's GitHub repository with examples
Top comments (4)
We should consider using the Intersection Observer API rather than handle scroll.
Want to submit a post request?
I cloned GitHub repository code, and changed authToken & endPoint. But it didn't work! I could only see blank page. Please tell me, What happen!
What was the error?