ruby erb 66 lines · 4 tabs

Turbo Streams: Create with prepend + HTML fallback

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

  def create
    @comment = @post.comments.build(comment_params)

    respond_to do |format|
      if @comment.save
        format.turbo_stream
        format.html { redirect_to @post, notice: "Comment added." }
      else
        format.turbo_stream do
          render turbo_stream: turbo_stream.replace(
            view_context.dom_id(@post, :new_comment),
            partial: "comments/form",
            locals: { post: @post, comment: @comment }
          ), status: :unprocessable_entity
        end
        format.html do
          @comments = @post.comments.order(created_at: :desc)
          render "posts/show", status: :unprocessable_entity
        end
      end
    end
  end

  private

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

  def comment_params
    params.require(:comment).permit(:body, :author)
  end
end
4 files · ruby, erb Explain with highlit

This snippet shows the canonical Hotwire pattern for creating a record and prepending it to a list over a Turbo Stream, while still working for clients that don't accept text/vnd.turbo-stream.html. The point is to write the controller once and respond to two formats from the same action, so behavior degrades gracefully instead of forking into two codepaths.

In CommentsController, the create action builds a comment scoped to a parent @post and branches on respond_to. On the happy path it hands off to format.turbo_stream, which renders the create.turbo_stream.erb template by convention. Critically, the same if @comment.save guard drives the format.html branch, which issues a plain redirect_to. When validation fails, the HTML branch re-renders :new with :unprocessable_entity so the browser shows errors normally. This dual response is what makes the feature robust: a request without JavaScript, or with Turbo disabled, still gets a working server-rendered flow.

The create.turbo_stream.erb template is where the surgical DOM update happens. turbo_stream.prepend targets the DOM id comments and renders the comments/comment partial, so the newest comment slides in at the top of the list without a full reload. A second turbo_stream.replace swaps the new_comment form back to a fresh, empty one — this is a common gotcha, since Turbo leaves the submitted form in place otherwise, retaining stale input. Emitting multiple stream actions from one template is fully supported and keeps related UI changes atomic.

The index.html.erb view wires everything together. The tag.div id: "comments" establishes the target the stream prepends into, and render @comments reuses the exact same partial the stream renders, so a page load and a live append produce identical markup. dom_id(@post, :new_comment) on the form_with gives the form a stable id that turbo_stream.replace can find.

The trade-off is a little template duplication in exchange for one authoritative partial and no client-side rendering code. Reaching for this pattern makes sense whenever a create should feel instant but must remain accessible and crawlable without relying on client JavaScript.


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: Create with prepend + HTML fallback — share card
Link copied