As a software engineer whenever I see something interesting in a web application I try to think about the way it was implemented. One of the things that caught my attention was the infinite scrolling functionality on sites like Facebook or Twitter.
Whenever you scroll to the end of the current content, a new batch of content will be loaded allowing you to continuously explore available content.
As I explored how infinite scrolling is implemented I thought what better way to synthesize this knowledge than trying to implement that in R/Shiny?
<ul><li><a href="#scrollvspagination">Why use Infinite Scroll vs Pagination</a></li><li><a href="#howtoadd">How to add Infinite Scroll to your R Shiny app</a></li><li><a href="#example">Example of Infinite Scroll in Shiny</a></li></ul>
<hr />
<h2 id="scrollvspagination">Why use Infinite Scroll vs Pagination?</h2>
Infinite scrolling allows you to display chunks of content to users in a performant manner. A different way of achieving this is the use of <a href="https://community.rstudio.com/t/fast-big-data-tables-in-shiny/121358#pagination-1" target="_blank" rel="nofollow noopener">pagination</a>. However, even though they achieve similar performance goals, the way users interact with them is different.
<blockquote>Shiny application layouts are changing. Explore the past and <a href="https://appsilon.com/shiny-application-layouts/" target="_blank" rel="noopener">see what's new in Shiny web app layouts</a>.</blockquote>
With infinite scrolling, the motion of scrolling leads to a new batch of data being shown on the page whenever the user reaches the end of the currently displayed content. In the case of pagination, the user can always see how many pages of data they can explore and they can select a specific page of data to be displayed.
<a href="https://blog.hubspot.com/website/pagination-vs-infinite-scroll#:~:text=Pagination%20works%20best%20on%20websites,very%20effective%20on%20mobile%20devices." target="_blank" rel="nofollow noopener">The Pagination vs Infinite Scroll</a> summarizes well the differences between those approaches and highlights their particular advantages. For example, infinite scrolling is easier to use on mobile devices and requires fewer “clicks” compared to pagination. As a result, it might increase user engagement on your page as viewers stay on your website longer; continuously searching for relevant content when they have no particular goal in mind.
<h2 id="howtoadd">How to add Infinite Scroll to your app</h2>
With the tooling available today we can implement infinite scrolling in a Shiny app with only a couple lines of JavaScript code.
<h3>What is Infinite Scroll in Shiny based on?</h3>
The <a href="https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API" target="_blank" rel="nofollow noopener">Intersection Observer API</a> is a <a href="https://developer.mozilla.org/en-US/docs/Web/API" target="_blank" rel="nofollow noopener">Web API</a> supported by most modern browsers - it allows you to observe changes in the intersection of a target element with the browser’s viewport (you can think of it as the browser window).
<blockquote>Is your dashboard getting slow? <a href="https://appsilon.com/r-shiny-caching/" target="_blank" rel="noopener">Try R Shiny caching options to increase the speed and responsiveness of your dashboard</a>.</blockquote>
<h3>How to set up Infinite Scroll in Shiny?</h3>
For the purpose of the post let’s assume a simple example: we want to show an infinite list of randomly generated numbers.
<pre><code>
library(shiny)
<br>generate_random_numbers_divs <- function(n) {
lapply(seq_len(n), function(i) div(runif(1)))
}
<br>ui <- fluidPage(
generate_random_numbers_divs(100)
)
<br>server <- function(input, output, session) {
}
<br>shinyApp(ui, server)
</code></pre>
Now, let’s set up our Intersection Observer. To do that we will also need to add a div that will be placed at the end of the list of our numbers. We will use it to detect the moment users reach the end of the list:
<pre><code>
library(shiny)
<br>generate_random_numbers_divs <- function(n) {
lapply(seq_len(n), function(i) div(runif(1)))
}
<br>
ui <- fluidPage( tags$head( tags$script(HTML(' $(document).ready(function() { const observer = new IntersectionObserver(function(entries) { if (entries[0].intersectionRatio > 0) {
alert("Reached end of list!")
}
});
<br> observer.observe(document.querySelector("#end"));
})
'))
),
generate_random_numbers_divs(100),
div(id = "end")
)
<br>server <- function(input, output, session) {
}
<br>shinyApp(ui, server)
</code></pre>
Now, if you run the app and scroll to the end of the list you will see an alert popup:
<video width="100%" height="auto" src="https://wordpress.appsilon.com/wp-content/uploads/2022/11/infinite_scroll_alert_rshiny.mp4" loop="true" autoplay="true" controls="true"></video>
We are now able to detect when a user reaches the end of the list. The next step is to inform Shiny about it and react appropriately by loading additional random numbers:
<pre><code>
library(shiny)
<br>generate_random_numbers_divs <- function(n) {
lapply(seq_len(n), function(i) div(runif(1)))
}
<br>
ui <- fluidPage( tags$head( tags$script(HTML(' $(document).ready(function() { const observer = new IntersectionObserver(function(entries) { if (entries[0].intersectionRatio > 0) {
Shiny.setInputValue("list_end_reached", true, { priority: "event" });
}
});
<br> observer.observe(document.querySelector("#end"));
})
'))
),
generate_random_numbers_divs(100),
div(id = "end")
)
<br>server <- function(input, output, session) {
observeEvent(input$list_end_reached, {
random_numbers_items_batch <- generate_random_numbers_divs(100)
insertUI(
selector = "#end",
where = "beforeBegin",
ui = random_numbers_items_batch
)
})
}
<br>shinyApp(ui, server)
</code></pre>
And we are done! We have an app that will infinitely generate new batches of random numbers as the user scrolls.
To add new items to the UI we make use of the insertUI function and define that the new content should be inserted before our #end div begins.
<video width="100%" height="auto" src="https://wordpress.appsilon.com/wp-content/uploads/2022/11/infinite_scroll_rshiny_final.mp4" loop="true" autoplay="true" controls="true"></video>
<h2 id="example">Example of Infinite Scroll in Shiny</h2>
In this post, we described how to implement infinite scrolling in a Shiny app. However, our example is a bit raw and could use some improvements in terms of styling. For example, you could add a loader informing the user that a new batch of content is being generated.
<blockquote>There's a lot you can do with Shiny. <a href="https://templates.appsilon.com/" target="_blank" rel="noopener">Download a free Shiny template</a> to get started and <a href="https://shiny.tools/" target="_blank" rel="noopener">browse our open-source packages</a> to level up your Shiny project.</blockquote>
You can find a more advanced example on my <a href="https://github.com/szymanskir/shiny-infinite-scroll" target="_blank" rel="nofollow noopener">github</a> [<a href="https://ryszard-szymaski.shinyapps.io/shiny_infinite_scroll/" target="_blank" rel="nofollow noopener">live app</a>], where you can scroll through the universe of Rick and Morty characters (powered by the <a href="https://rickandmortyapi.com/" target="_blank" rel="nofollow noopener">Rick and Morty API</a>).
<video width="100%" height="auto" src="https://wordpress.appsilon.com/wp-content/uploads/2022/11/Rick-and-Morty-Infinite-Scroll-in-R-Shiny.mp4" loop="true" autoplay="true" controls="true"></video>
If you find the Shiny Infinite Scroll helpful, please be sure to give it a star on GitHub. And if you come up with an interesting use, be sure to share it with us at Appsilon. We love to see the community's creative applications!
<blockquote>Ready to scale your Shiny app? <a href="https://www.youtube.com/watch?v=iI5L8qmmZaU&t=5s" target="_blank" rel="noopener">Watch Pedro Silva's workshop on how to scale Shiny and infrastructure</a>.</blockquote>