ruby erb 61 lines · 3 tabs

Use data-turbo-action to control history (advance vs replace)

Shared by codesnips Jan 2026
3 tabs
class ProductsController < ApplicationController
  PER_PAGE = 24

  def index
    @page = [params[:page].to_i, 1].max
    @sort = %w[name price recent].include?(params[:sort]) ? params[:sort] : "recent"

    scope = Product.available.order(sort_column(@sort))
    @total_pages = (scope.count.to_f / PER_PAGE).ceil
    @products = scope.offset((@page - 1) * PER_PAGE).limit(PER_PAGE)
  end

  private

  def sort_column(sort)
    case sort
    when "name"  then { name: :asc }
    when "price" then { price_cents: :asc }
    else { created_at: :desc }
    end
  end
end
3 files · ruby, erb Explain with highlit

This snippet demonstrates how Hotwire's Turbo Drive and Turbo Frames interact with the browser History API through the data-turbo-action attribute. When a Turbo Frame navigates, it normally does not touch the URL or push a history entry. Setting data-turbo-action="advance" promotes a frame navigation into a full history push, so the frame's src becomes the visible URL and a new back-button entry is created. Setting data-turbo-action="replace" instead swaps the current entry in place, changing the URL without growing the history stack.

The ProductsController in the first tab is a plain Rails index action with keyset-free offset pagination via a page param. It renders normally on full loads and renders the same view when a frame requests a page, because Turbo Frame requests carry a Turbo-Frame header that Rails handles transparently — no special branch is needed here.

The index.html.erb tab wraps the product grid and pager inside a turbo_frame_tag named products. The key detail is data: { turbo_action: "advance" } on the frame: this tells Turbo that any navigation landing in this frame should advance history and update the address bar. As a result, paginating deep into the catalog leaves the browser back button working exactly as a user expects, and the current page number is shareable and bookmarkable.

The _pager.html.erb partial shows the trade-off in miniature. The numbered page links inherit advance from the frame, so each click is a distinct history entry. The compact "filter" style links use data: { turbo_action: "replace" } at the link level, which overrides the frame default for that single navigation — appropriate for state that should not pollute history, like toggling a sort order the user will tweak repeatedly.

The practical rule is: use advance for navigations a user would want to reverse with the back button, and replace for incidental state changes. A common pitfall is enabling advance on a frame whose src diverges from the real controller route, producing URLs that 404 on refresh; keeping frame src values aligned with actual routes, as done in _pager.html.erb, avoids that.


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.

Use data-turbo-action to control history (advance vs replace) — share card
Link copied