ruby erb scss 56 lines · 4 tabs

Infinite scrolling list using lazy Turbo Frames

Shared by codesnips Jan 2026
4 tabs
class PostsController < ApplicationController
  def index
    @posts = Post.published
                 .order(created_at: :desc)
                 .page(params[:page])
                 .per(20)
  end
end
4 files · ruby, erb, scss Explain with highlit

This snippet builds an infinite scrolling list using Hotwire Turbo Frames with no custom JavaScript, relying instead on the loading: :lazy behavior of a frame that only fetches its contents when it scrolls into the viewport. The pattern works by rendering the current page of records, then appending a single trailing frame whose src points at the next page. As the user scrolls and that frame becomes visible, Turbo issues a GET request, replaces the frame with the next batch, and that batch in turn contains its own trailing lazy frame — chaining requests one page at a time until the collection is exhausted.

In PostsController, the index action paginates with Kaminari via Post.page(params[:page]) and renders the same template for both the initial full-page load and the subsequent frame requests. Because a lazy frame request is just an ordinary GET to the same URL with a page param, no separate endpoint or respond_to branch is required; Turbo automatically extracts the matching frame from the response by its id.

The index.html.erb tab holds the outer structure and renders the shared posts/list partial. That partial in _list.html.erb is the reusable unit: it iterates the posts, then conditionally emits a turbo_frame_tag with loading: :lazy and a src built from posts_path(page: posts.next_page). The frame id is derived from the page number so each generated frame is unique, which matters because Turbo matches frames by DOM id and duplicate ids would break the swap.

The key detail is the guard if posts.next_page — when Kaminari reports no further pages, the trailing frame is omitted entirely, which naturally terminates the chain. A spinner inside the frame gives visual feedback during the fetch and is discarded when the frame's real content arrives.

The trade-off is that this approach loads pages strictly sequentially and depends on the frame actually entering the viewport, so very short lists or unusual layouts may never trigger the load. It is ideal for feeds and search results where progressive disclosure is acceptable and shipping zero bespoke JavaScript is worth more than fine-grained control over prefetching.


Related snips

ruby
class CommentsController < ApplicationController
  before_action :set_post

  def create
    @comment = @post.comments.build(comment_params)

System test: asserting Turbo Stream responses

rails hotwire turbo
by codesnips 4 tabs
ruby
class Post < ApplicationRecord
  belongs_to :author, class_name: 'User'
  has_many :comments, dependent: :destroy

  scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
  scope :draft, -> { where(published_at: nil) }

ActiveRecord scopes for reusable query logic

rails activerecord patterns
by Alex Kumar 1 tab
ruby
module Api
  module V1
    class UsersController < BaseController
      def show
        user = User.includes(:profile).find(params[:id])

ETags for conditional requests and caching

rails caching http-caching
by Alex Kumar 1 tab
ruby
class PostsController < ApplicationController
  def index
    @posts = Post.includes(:author)
                 .order(created_at: :desc)
                 .page(params[:page])
                 .per(10)

Turbo Frames: infinite scroll with lazy-loading frame

rails turbo hotwire
by codesnips 4 tabs
ruby
require "csv"

class PeopleCsvStream
  include Enumerable

  HEADERS = %w[id full_name email signed_up_at plan].freeze

Resilient CSV Export as a Streamed Response

rails performance streaming
by codesnips 3 tabs
erb
<form data-controller="query-sync" data-action="change->query-sync#apply">
  <select name="status" class="rounded border p-2">
    <option value="">Any</option>
    <option value="open">Open</option>
    <option value="closed">Closed</option>
  </select>

Filter UI that syncs query params via Stimulus (no front-end router)

rails hotwire stimulus
by Henry Kim 2 tabs

Share this code

Here's the card — post it anywhere.

Infinite scrolling list using lazy Turbo Frames — share card
Link copied