ruby erb 81 lines · 4 tabs

Declarative model broadcasts with broadcasts_to (Rails 7)

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

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

  broadcasts_to ->(comment) { [comment.post, :comments] },
                inserts_by: :append,
                target: ->(comment) { "post_#{comment.post_id}_comments" }

  scope :chronological, -> { order(created_at: :asc) }

  def edited?
    updated_at - created_at > 1.second
  end
end
4 files · ruby, erb Explain with highlit

This snippet shows how Rails 7 turns Active Record callbacks into live UI updates using broadcasts_to, without hand-writing any WebSocket plumbing. The pattern solves a common problem: keeping every open browser in sync when a record is created, updated, or destroyed. Instead of manually calling Turbo::StreamsChannel.broadcast_* from a controller or job, the model declares which stream it belongs to and Turbo emits the right stream action after each commit.

In Comment model, broadcasts_to :post wires three callbacks at once: an append on create, a replace on update, and a remove on destroy, all scoped to the stream named after the associated post. The ->(comment) { [comment.post, :comments] } lambda builds a compound stream identifier so each post gets its own isolated channel. Because broadcasts fire on the after-commit boundary, a rolled-back transaction never leaks a phantom update to subscribers, which is why this is safer than broadcasting from a before_save. The target: option matches the DOM id that the view renders, and Rails resolves the partial by convention (comments/_comment).

In PostsController, notice how little the controller does: @comment = @post.comments.create!(comment_params) is the entire real-time path. The controller responds to a normal HTML request; the broadcast happens as a side effect of the successful commit, so the originating user and every other subscriber receive the same stream. This keeps the request/response cycle honest and avoids duplicating rendering logic between the controller and the broadcaster.

In posts/show view, turbo_stream_from @post, :comments opens the subscription whose name matches the model's stream lambda, and the comments list is wrapped in a container whose id lines up with the target. New <li> elements are appended automatically as other users post.

The trade-offs are worth knowing: broadcasts_to renders partials inside the broadcasting process, so heavy templates can slow commits — the broadcasts_to ... later variant offloads rendering to a job. It also assumes the DOM ids and partial names follow convention, so drift between the model, view, and target will silently drop updates. For per-record fan-out with predictable naming, though, it removes an enormous amount of boilerplate.


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.

Declarative model broadcasts with broadcasts_to (Rails 7) — share card
Link copied