In this blog post, I will walk you through creating an infinite scroll feature using Rails and Turbo. I wanted to see if using only Turbo was an option for this feature, and it turns out it is. I was excited to discover this, so I wanted to share the wealth.
I will be sure to 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, 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 use. There are many more options for pagination with Rails, 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 a little faster.
Top comments (1)
Just implemented it and it works out of the box, amazing! without any JS workarounds! Thank you