javascript erb ruby 75 lines · 3 tabs

Infinite scroll with Turbo Frames and Intersection Observer

Jordan Lee Jan 2026
3 tabs
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["sentinel"]
  static values = {
    url: String
  }

  connect() {
    this.observer = new IntersectionObserver(
      entries => this.handleIntersection(entries),
      { rootMargin: "100px" }
    )

    if (this.hasSentinelTarget) {
      this.observer.observe(this.sentinelTarget)
    }
  }

  disconnect() {
    this.observer.disconnect()
  }

  handleIntersection(entries) {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        // Trigger the frame to load
        this.sentinelTarget.src = this.urlValue
        this.observer.unobserve(this.sentinelTarget)
      }
    })
  }
}
3 files · javascript, erb, ruby Explain with highlit

Infinite scroll improves perceived performance by loading content as users reach the bottom of the page. I combine Turbo Frames with the Intersection Observer API via a Stimulus controller. The last item in each page has a sentinel element that, when visible, triggers a frame load for the next page. The server returns the next page wrapped in the same frame ID, which replaces the sentinel with new content plus a new sentinel. This approach is more efficient than scroll event listeners and works seamlessly with Turbo's caching. I also provide a fallback 'Load More' button for users with JavaScript disabled or those who prefer manual control.


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 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
ruby
class TasksController < ApplicationController
  def destroy
    @task = Task.find(params[:id])
    @task.destroy!

    respond_to do |format|

Remove deleted items instantly with turbo_stream.remove

rails hotwire turbo
by Henry Kim 2 tabs
ruby
Rails.application.configure do
  config.after_initialize do
    Bullet.enable = true
    Bullet.alert = false
    Bullet.bullet_logger = true
    Bullet.console = true

N+1 query detection with Bullet gem

rails performance activerecord
by Alex Kumar 2 tabs

Share this code

Here's the card — post it anywhere.

Infinite scroll with Turbo Frames and Intersection Observer — share card
Link copied