Adding Infinite scroll in Phoenix LiveView App

Adding Infinite scroll in Phoenix LiveView App

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.

What is Infinite Scroll?

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. infinite-scroll.gif

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.

How Infinite Scroll works?

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.

What is JS hook?

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.

How does JS Hook work?

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.

Adding temporary_assigns

One 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

Adding the hook in LiveSocket

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.

Calling the Hook

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.

  • Hooks (phx-hook="InfiniteList")
  • phx-update (phx-update=append)
  • data attribute (data-page={@page_no})
  • id (id='infinite-list')

Conclusion

For performing Infinite scroll we have to

  • Create hooks
  • Add it to the hooks list in LiveSocket object
  • Add hook 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 😊.

References

Did you find this article valuable?

Support AbulAsar S. by becoming a sponsor. Any amount is appreciated!