ruby erb javascript 103 lines · 4 tabs

Turbo Stream flash messages without custom JS

Shared by codesnips Jan 2026
4 tabs
module FlashHelper
  FLASH_CLASSES = {
    "notice" => "flash--success",
    "alert"  => "flash--error",
    "error"  => "flash--error"
  }.freeze

  def flash_class(type)
    FLASH_CLASSES.fetch(type.to_s, "flash--info")
  end

  def turbo_flash
    flash.map do |type, message|
      turbo_stream.append "flash", partial: "shared/flash",
                                   locals: { type: type, message: message }
    end.join.html_safe
  end
end
4 files · ruby, erb, javascript Explain with highlit

This snippet shows how a Rails app can render flash messages over Turbo Stream responses without writing any custom JavaScript. The pattern relies on Turbo's built-in stream actions (append, update, remove) targeting a stable DOM container, so ordinary controller actions that respond with turbo_stream can push toasts into the page as a side effect of any create/update/destroy.

The flash partial defines the visual container #flash plus one partial per message. Each toast carries a data-controller="removals" hook whose only job is to auto-dismiss after a delay — this is a tiny, generic Stimulus controller, not feature-specific glue, and it degrades gracefully if JS is disabled since the message still renders. Giving each toast a dom_id based on the flash type keeps replacements idempotent.

The FlashHelper centralizes the mapping from Rails flash keys (:notice, :alert) to CSS classes via flash_class, and exposes turbo_flash which wraps turbo_stream.append "flash" for every pending flash entry. Rendering the partial through the helper means both full-page loads and stream responses share exactly one template, avoiding drift between the two rendering paths.

In ArticlesController, the create action responds to turbo_stream by concatenating two streams: one that prepends the new article row and one produced by turbo_flash. Because flash.now is used, the message lives only for this response and is not persisted into the session — important for stream responses, which do not perform a redirect and would otherwise leave a stale flash for the next request. The destroy action follows the same shape, removing the row and emitting a confirmation toast.

The main trade-off is that the #flash container must already exist in the layout for append to find a target; if it is missing, Turbo silently drops the stream. The approach shines when an app already uses Turbo for CRUD and wants consistent feedback without a bespoke notification stack. Pitfalls to watch include double-rendering flashes when a redirect is mixed with a stream, and forgetting flash.now, which reintroduces the classic "message shows one action too late" bug.


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 Stream flash messages without custom JS — share card
Link copied