Adding Infinite scroll in Phoenix LiveView App

Search for a command to run...

Nice article!
It's worth mentioning that if you scroll for long enough time, socket.assigns.blogs would grow - possibly a lot. Unless you don't plan on live updating them, you can use temporary assigns. LiveView docs have the whole section about it.
That's an excellent point Paweł Świątkowski, sure I'll edit the blog and add this point.
Motivation Recently, I tried to learn some low-level system programming stuff. I am a Mac user, and I thought that everything that works on Linux should also work on Mac. After all, Mac is a Unix-based system 😊. I guess we all heard this. Oh boy! I ...

Problem Statement While working on a Nestjs project, I encountered a weird problem related to the database column. I was trying to insert a record into a MySQL table using TypeORM. The error I was experiencing stated that a specific column “cannot be...

Recently, I was exploring design patterns courses on my LinkedIn Learning subscription. I came across a course, Node.js: Design Patterns by Alex Banks. It is a wonderful, easy-to-understand course. I started with the Builder Pattern, and the explanat...

Suppose you are working on a table in a LiveView project. This table has limited static data of not more than one page (you can avoid questions about pagination in the comment section 😊). From a user's point of view, it becomes hard to look into the...

Recently, while working on one of my personal Elixir projects. I came across a scenario where I needed to make some changes to the database schema.The changes mainly revolve around adding and removing indexes due to changes in the business requiremen...

In my previous blog, I explained how to query Graphql API using Neuron. Throughout the blog, I explained the working of Neuron on page 0. Whereas, there will be scenarios when users can have multiple blogs paginated on multiple pages. So, we need to add functionality so that users can load multiple pages. This can be achieved easily by adding a button with some text Load More and adding an event to load the data on the click. But we will be using a little different approach of using paginating using Infinite Scroll. This will be a continuation of my previous blog, so let's add this feature.
Before we start, we first need to understand how Infinite scroll works. To get a fair idea about Infinite Scroll, refer to the gif image below.

This is similar to what we see on Youtube, Facebook, Instagram, etc. It keeps user engage in the content and also keep away user from the hassle of clicking the Next button to load content.
Infinite Scroll logic is very simple, we just have to add a javascript event listener which will listen to the scroll event. It will get triggered as soon as we reach the bottom (near the bottom) of the page. Below is the logic to detect the bottom of the page.
let scrollAt = () =>
let scrollTop = document.documentElement.scrollTop || document.body.scrollTop
let scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight
let clientHeight = document.documentElement.clientHeight
return scrollTop / (scrollHeight - clientHeight) * 100
}
We just need to call this logic in the proper LiveView lifecycle and this can be achieved with LiveView JS Hooks.
Phoenix LiveView often markets itself as that we can create reactive apps without writing JavaScript. But still, there comes a time when you have to write javascript. To integrate Javascript in our LiveView app, LiveView provides us with a concept of LiveView JS Hooks. JS Hooks allow us to register JavaScript lifecycle callbacks on DOM nodes.
There are different types of lifecycle callbacks like mounted, reconnected, and updated. The mounted is the first callback that is called and initialized with some initial value as soon as the element on which we are applying it is mounted in the DOM. So, here we will add our addEventListener and initialize some value. We will create a new file for our hook file in assets/js/infinite_list.js and add the following code.
let scrollAt = () => {
let scrollTop = document.documentElement.scrollTop || document.body.scrollTop
let scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight
let clientHeight = document.documentElement.clientHeight
return scrollTop / (scrollHeight - clientHeight) * 100
}
InfiniteList = {
page() { return this.el.dataset.page },
mounted(){
this.pending = this.page()
window.addEventListener("scroll", e => {
if(this.pending == this.page() && scrollAt() > 90){
this.pending = this.page() + 1
this.pushEvent("load-blogs", {})
}
})
},
reconnected(){ this.pending = this.page() },
updated(){ this.pending = this.page() }
}
export default InfiniteList
As discussed mounted callback is a place where we will add eventListener logic. It detects page bottom and triggers pushEvent of load-blogs. This will call handle_event callback in our lib/fetch_hashnode_web/live/blogs_live.ex which will have the following code logic.
def handle_event("load-blogs", _params, socket) do
page_no = socket.assigns.page_no + 1
new_blogs = page_no |> Blog.get_blogs
blogs = socket.assigns.blogs ++ new_blogs
socket = assign(socket, blogs: blogs, page_no: page_no)
{:noreply, socket}
end
The logic here is self-explanatory we are fetching blogs using Blog.get_blogs by passing incremented page_no to it and assigning the blogs to the socket struct.
temporary_assignsOne last change we have to make in the blogs_live.ex code. We have to use temporary_assigns for our blogs field in the mount callback. When we assign new blogs to socket.assigns.blogs there are chances that our data can grow, which will increase data in the socket, and ultimately our application performance will be affected. Refer to the following change
def mount(_params, _session, socket) do
blogs = Blog.get_blogs()
socket = assign(socket, blogs: blogs, page_no: 0)
{:ok, socket, temporary_assigns: [blogs: []]}
end
Now we will include our InfiniteList hook in our LiveSocket. For this, we will open the app.js file and import InfiniteList and include it in our Hooks object, and add the Hooks to our LiveSocket constructor.
import InfiniteList from "./infinite_list"
let Hooks = { InfiniteList }
....
let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}, hooks: Hooks })
// Notice `hooks: Hooks`
Now that we have added our InfiniteList hook to our hooks list. We will call this hook in our final arrangement.
To call our Javascript logic i.e hook we’ll add the phx-hook attribute on the div with value as the hook name i.e InfiniteList where we want to perform scrolling.
<div class="blogs" id="infinite-list" phx-hook="InfiniteList" phx-update="append" data-page={@page_no}>
<%= for blog <- @blogs do %>
// Display logic
<% end %>
</div>
You will also notice the data-page attribute which has the value of @page_no. This attribute is the reason, that we were able to access the page number in the page method of the InfiniteScroll hook.
InfiniteList = {
page() { return this.el.dataset.page },
...
The phx-update=append helps us to append or prepend the updates rather than replacing the existing contents. And don't miss to add the id attribute as well on the div. So, the following is the list of attributes that we need to apply on the div to enable javascript interactions.
phx-hook="InfiniteList")phx-update=append)data-page={@page_no})id='infinite-list')For performing Infinite scroll we have to
LiveSocket objecthook on the div where you want to perform scrolling so that it can communicate with the javascript.I hope you like this blog. If you have any questions then please comment below. Thanks for reading 😊.