Andy from Webcrunch

Subscribe for email updates:

Portrait of Andy Leverenz
Andy Leverenz

February 26, 2024

Last updated February 27, 2024

Infinite Scroll with Rails and Turbo - No JavaScript

In this blog post, I will walk you through the process of creating an infinite scroll feature using Rails and Turbo. I wanted to see if using only Turbo was an option for this feature and turns out, it is. I was excited to discover this so I wanted to share the wealth.

I will cover the necessary steps, including setting up the database and handling the server-side logic. By the end of this guide, you will have a fully functional infinite scroll feature in your Rails application that you can borrow on your own.

Important: Make sure you’re using the latest version of Rails and turbo-rails gem.

Create a new app

rails new infinite_scroll_turbo -j esbuild

Add Tailwind CSS, will_paginate, and faker gems

To make this demo more realistic, I’ll use the tailwindcss-rails gem for some ready-to-go styles out of the box.

If you'd like more control consider passing -c tailwind in your rails new command which gives you a blank slate OR if you want some UI done for you check out my project Rails UI.

bundle add tailwindcss-rails faker will_paginate
rails tailwindcss:install

Generate some records

First off, I’ll set up the development database and house some dummy content we’ll generate with the faker gem we just installed.

rails generate scaffold Post title:string content:text
rails db:migrate

Add some Ruby code to seed data more quickly inside db/seeds.rb

# db/seeds.rb
50.times do
  Post.create(title: Faker::Lorem.sentence(word_count: 4), content: Faker::Lorem.paragraph(sentence_count: 4))
end

Finally, run the seed command to generate those records.

rails db:seed

Update Rails routing

Since we scaffolded a Post model I'll root the app to posts#index

# config/routes.rb
Rails.application.routes.draw do
  resources :posts
  get "up" => "rails/health#show", as: :rails_health_check
  root "posts#index" # uncomment this line!
end

With our database and seeded data in the app, we’re ready to consider the controller logic.

To make infinite scrolling work we’ll need some form of pagination.

I reached for will_paginate because it’s super simple to set up and make use of. There are many more options for pagination with Rails out there so feel free to swap for something you prefer.

Update the posts controller

In the PostsController I added the following code.

# app/controllers/posts_controller.rb 

def index
  @posts = Post.paginate(page: params[:page], per_page: 4)
end

Infinite scroll logic with Rails and Turbo

To use no JavaScript we’ll use a unique way to render a index.turbo_stream.erb file in the form of a turbo stream response. Here's the code in my posts/index.html.erb file.

<div class="w-full">
  <% if notice.present? %>
    <p class="py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block" id="notice"><%= notice %></p>
  <% end %>

  <div class="flex justify-between items-center">
    <h1 class="font-bold text-4xl">Posts</h1>
    <%= link_to "New post", new_post_path, class: "rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium" %>
  </div>

  <div id="posts" class="min-w-full">
    <%= turbo_frame_tag "posts", src: posts_path(format: :turbo_stream), loading: :lazy %>
  </div>

  <% if @posts.next_page %>
    <%= turbo_frame_tag "load_more", src: posts_path(page: @posts.next_page, format: :turbo_stream), loading: :lazy %>
  <% end %>
</div>

<div class="fixed bottom-0 rounded-tl right-0 h-12 bg-gray-50/50 backdrop-blur-sm w-full text-lg font-medium py-2 text-gray-900 text-center border-t border-l border-gray-200/80 w-56">
  <p>🚀 Current page: <span id="current_page"><%= @posts.current_page %></span></p>
</div>

Using the turbo_frame_tag we can source a defined path in the Rails app to load lazily. This just works which is impressive in itself.

Real-time infinite pagination

I passed an explicit format to the path helpers (i.e. posts_path(format: :turbo_stream) ).

Doing this instructs the rails app to return index.turbo_stream.erb view logic instead of the default index.html.erb file using the advertised “HTML over the wire” approach.

Create a new file called index.turbo_stream.erb in your app/views/posts folder. Inside the index.turbo_stream.erb file, I added the following:

<%= turbo_stream.append "posts" do %>
  <%= render partial: "posts/post", collection: @posts %>
<% end %>

<% if @posts.next_page %>
  <%= turbo_stream.replace "load_more" do %>
    <%= turbo_frame_tag "load_more", src: posts_path(page: @posts.next_page, format: :turbo_stream), loading: :lazy %>
  <% end %>
<% end %>

<%= turbo_stream.update "current_page", @posts.current_page %>

Using will_paginate, we can conditionally check for a next_page based on the parameters set in the controller.

If a new page is present, I display the turbo_frame_tag “load_more” in the index.html.erb and index.turbo_stream.erb views.

This dynamically loads via the paths passed to the src argument on the turbo_frame_tag.

Notice all formats are loaded as :turbo_stream. This continues happening recursively until no pages are left as you scroll.

To make each post more resemble a blog post, I updated the markup and styling slightly inside _post.html.erb.

<article id="<%= dom_id post %>" class="py-6">

  <h1 class="text-2xl font-semibold tracking-tight"><%= post.title %></h1>

  <p class="my-5 text-lg text-gray-700">
    <%= post.content %>
  </p>

  <% if action_name != "show" %>
    <%= link_to "Show this post", post, class: "rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
    <%= link_to "Edit this post", edit_post_path(post), class: "rounded-lg py-3 ml-2 px-5 bg-gray-100 inline-block font-medium" %>
    <hr class="mt-6">
  <% end %>
</article>

Done!

And with that, we have infinite scroll using Ruby on Rails and Turbo 8. No additional JavaScript/Stimulus.js is necessary. I think this is a game changer and I hope this tip helps you code your Rails app just a little bit faster.

Keep reading

Link this article
Est. reading time: 5 minutes
Stats: 591 views

Categories

Collection

Part of the Hotwire and Rails collection