ruby erb 100 lines · 4 tabs

Broadcast Live Comment Updates with Turbo Streams in Rails

Shared by codesnips Aug 2026
4 tabs
class Comment < ApplicationRecord
  belongs_to :post
  belongs_to :author, class_name: "User"

  validates :body, presence: true, length: { maximum: 2_000 }

  default_scope { order(created_at: :asc) }

  after_create_commit do
    broadcast_prepend_later_to(
      [post, :comments],
      target: "comments",
      partial: "comments/comment",
      locals: { comment: self }
    )
  end

  after_update_commit do
    broadcast_replace_later_to([post, :comments], target: self)
  end

  after_destroy_commit do
    broadcast_remove_to([post, :comments], target: self)
  end
end
4 files · ruby, erb Explain with highlit

This snippet shows how a live comment feed is kept in sync across every connected browser using Turbo Streams and Action Cable, without any custom JavaScript for the append logic. The core idea is that the server owns the DOM diff: instead of returning JSON and reconstructing markup on the client, Rails renders a partial server-side and pushes a turbo_stream frame over a WebSocket to a named stream that clients subscribe to.

In Comment model, the broadcasts_to macro wires the model into a stream keyed by its parent post. Every create, update, and destroy automatically triggers a broadcast to [post, :comments], so no controller code is needed for the happy path. The custom after_create_commit callback overrides the default create behaviour to broadcast_prepend_later_to, which enqueues the render on a background job so the request returns immediately and the socket write happens off the request thread. broadcast_prepend_later_to is preferred over the synchronous variant because rendering ERB and pushing to Redis under an HTTP request adds latency and can fail independently of the write.

The target: of "comments" matches the DOM id of the container the stream mutates, and partial: plus locals: control exactly what HTML each subscriber receives. Because rendering happens in a job, locals must be serializable-friendly — passing the record and letting the partial resolve associations is the safe pattern.

In CommentsController, create responds to both turbo_stream and html formats. The turbo_stream branch is essentially a fallback: it updates the DOM of the submitting user immediately via the HTTP response, while broadcasts_to handles every other connected client. Resetting the form with turbo_stream.replace gives the submitter a cleared input.

In comments/show.html.erb, turbo_stream_from post, :comments opens the subscription — this helper renders the <turbo-cable-stream-source> element that Action Cable binds to. The container id must equal the broadcast target. A key pitfall is a mismatch between the stream name in the view and the model; both must resolve to the same signed stream identifier or broadcasts silently go nowhere. This pattern trades a little rendering cost on the server for zero client-side view logic and guaranteed markup consistency.


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.

Broadcast Live Comment Updates with Turbo Streams in Rails — share card
Link copied