erb ruby 81 lines · 4 tabs

Live counter updates with Turbo Streams (likes, votes)

Shared by codesnips Jan 2026
4 tabs
<%# Subscribe every viewer of this post to its broadcasts %>
<%= turbo_stream_from @post %>

<%# Update the acting user's button to reflect the toggle %>
<%= turbo_stream.replace dom_id(@post, :like_button) do %>
  <%= render "posts/like_button", post: @post %>
<% end %>
4 files · erb, ruby Explain with highlit

This snippet shows how a live like counter is built in Rails with Hotwire's Turbo Streams so that every connected client sees the count change the moment anyone clicks, without any custom JavaScript. The story spans four collaborating files: the Post model that broadcasts changes, the LikesController that toggles a like, the _like_button partial that renders the DOM target, and the like.turbo_stream.erb template that responds to the acting user.

In Post model, has_many :likes combines with counter_cache: true on the Like side so the likes_count column is maintained by the database association callbacks rather than a COUNT(*) on every render. The key detail is that increment_counter/decrement_counter (used by the counter cache) issue atomic UPDATE ... SET likes_count = likes_count + 1 statements, which avoids the read-modify-write race where two simultaneous likes would each read the same stale value and clobber each other. broadcast_replace_to is triggered from an after_update_commit callback keyed on saved_change_to_likes_count?, so a Turbo Stream frame is pushed only when the number actually changed, and only after the transaction commits.

In LikesController, the toggle is made idempotent per user with find_or_create_by/destroy_by guarded against duplicates, and the whole operation is wrapped so the counter stays consistent. Because the model already broadcasts to every subscriber, the controller's own respond_to only needs to update the button state for the person who clicked.

_like_button partial wraps the count in a turbo_frame-style target dom_id(post, :likes), which is exactly the id broadcast_replace_to targets, so the server-rendered fragment and the broadcast frame line up automatically. like.turbo_stream.erb uses turbo_stream.replace to swap the button so the current user sees the liked/unliked toggle immediately.

The main trade-off is that broadcast_replace_to re-renders the partial server-side for each broadcast, which is cheap for a counter but should be scoped narrowly. A common pitfall is broadcasting on every save rather than on the guarded saved_change_to_likes_count?, which floods clients with redundant frames. This pattern is the right reach when many viewers watch the same record and eventual real-time consistency matters more than per-request 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 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.

Live counter updates with Turbo Streams (likes, votes) — share card
Link copied