ruby erb javascript 83 lines · 4 tabs

Turbo Streams: swap a button state and counter in one response

Shared by codesnips Jan 2026
4 tabs
class LikesController < ApplicationController
  before_action :set_post

  def create
    @like = current_user.likes.create!(post: @post)
    respond(liked: true)
  rescue ActiveRecord::RecordNotUnique
    respond(liked: true)
  end

  def destroy
    current_user.likes.where(post: @post).destroy_all
    respond(liked: false)
  end

  private

  def set_post
    @post = Post.find(params[:post_id])
  end

  def respond(liked:)
    @liked = liked
    @count = @post.likes.count

    respond_to do |format|
      format.turbo_stream
      format.html { redirect_to @post }
    end
  end
end
4 files · ruby, erb, javascript Explain with highlit

This snippet demonstrates the Turbo Streams pattern for updating multiple, physically separate parts of a page from a single server response without writing any custom JavaScript for the DOM manipulation. The scenario is a classic "like" feature: clicking the button must flip the button's own state (from "Like" to "Liked") and independently update a counter that lives elsewhere in the layout. A single HTTP round trip produces a stream that carries both fragments.

The LikesController handles both create and destroy, toggling the association through a Like join model and rescuing ActiveRecord::RecordNotUnique so a double click can never blow up on the unique index. Rather than redirecting, the controller responds to format.turbo_stream, which Rails maps to a .turbo_stream.erb template. The HTML fallback in format.html keeps the feature usable when Turbo is disabled — progressive enhancement rather than a hard dependency.

In create.turbo_stream.erb, two turbo_stream actions are emitted in one response. The first replaces the DOM element whose id matches dom_id(@post, :like_button), and the second updates the counter element by id. Because each action targets a stable id, the button and the counter can sit anywhere in the document; Turbo finds them and swaps them in place. The destroy template mirrors this so the un-like path is symmetric.

The shared _like_button partial is the single source of truth for the button markup. It is rendered by the full page, by the stream, and could be rendered by a broadcast, so the state rendered on first load and the state rendered after a click are guaranteed to be identical. The wrapping id from dom_id is what lets replace target it precisely.

A key trade-off is that the server owns the rendering, which keeps state consistent but adds a round trip per click. The LikeButton controller layers optimistic UI on top: toggle immediately flips aria-pressed and disables the element so rapid clicks don't stack requests, then Turbo's incoming stream overwrites that guess with the authoritative markup. Reaching for this pattern makes sense whenever one action must reconcile several disconnected regions of a page cheaply and reliably.


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.

Turbo Streams: swap a button state and counter in one response — share card
Link copied