# Controller responding with Turbo Stream
class PostsController < ApplicationController
def create
@post = Post.new(post_params)
if @post.save
respond_to do |format|
format.turbo_stream do
render turbo_stream: turbo_stream.prepend(
"posts",
partial: "posts/post",
locals: { post: @post }
)
end
format.html { redirect_to @post }
end
else
render :new, status: :unprocessable_entity
end
end
def destroy
@post = Post.find(params[:id])
@post.destroy
respond_to do |format|
format.turbo_stream do
render turbo_stream: turbo_stream.remove(@post)
end
format.html { redirect_to posts_path }
end
end
def update
@post = Post.find(params[:id])
if @post.update(post_params)
respond_to do |format|
format.turbo_stream do
render turbo_stream: [
turbo_stream.replace(
@post,
partial: "posts/post",
locals: { post: @post }
),
turbo_stream.update(
"flash",
partial: "shared/flash",
locals: { message: "Post updated!" }
)
]
end
format.html { redirect_to @post }
end
end
end
end
# Broadcasting Turbo Streams (for real-time updates)
class Post < ApplicationRecord
after_create_commit do
broadcast_prepend_to "posts",
target: "posts",
partial: "posts/post",
locals: { post: self }
end
after_update_commit do
broadcast_replace_to "posts"
end
after_destroy_commit do
broadcast_remove_to "posts"
end
# Broadcast to specific users
after_create_commit do
broadcast_prepend_to(
[user, "posts"],
target: "user_posts",
partial: "posts/post"
)
end
end
# Multiple Turbo Stream actions
def multi_update
render turbo_stream: [
turbo_stream.replace("post_#{@post.id}", partial: "posts/post"),
turbo_stream.update("comments_count", @post.comments.count),
turbo_stream.append("notifications", partial: "shared/notification"),
turbo_stream.remove("draft_indicator")
]
end
<!-- app/views/posts/create.turbo_stream.erb -->
<%= turbo_stream.prepend "posts", @post %>
<%= turbo_stream.update "post_count", Post.count %>
<%= turbo_stream.replace "new_post_form" do %>
<%= render "form", post: Post.new %>
<% end %>
<!-- Multiple targeted updates -->
<%= turbo_stream.replace dom_id(@post) do %>
<%= render @post %>
<% end %>
<%= turbo_stream.update "flash" do %>
<div class="alert alert-success">Post created!</div>
<% end %>
<!-- Turbo Stream from Action Cable -->
<!-- app/views/posts/_post.html.erb -->
<%= turbo_stream_from "posts" %>
<div id="posts">
<%= render @posts %>
</div>
<!-- Subscribes to posts channel, receives real-time updates -->
<!-- Main page with turbo frame -->
<div class="posts">
<%= turbo_frame_tag "posts_list" do %>
<%= render @posts %>
<%= link_to "Load more", posts_path(page: @next_page),
class: "load-more" %>
<% end %>
</div>
<!-- Clicking "Load more" only replaces posts_list frame -->
<!-- Nested frames for independent updates -->
<div class="post">
<h1><%= @post.title %></h1>
<%= turbo_frame_tag dom_id(@post, :comments) do %>
<%= render @post.comments %>
<%= link_to "Load more comments",
post_comments_path(@post, page: @next_page) %>
<% end %>
<%= turbo_frame_tag dom_id(@post, :edit) do %>
<%= link_to "Edit post", edit_post_path(@post) %>
<% end %>
</div>
<!-- Edit form returns matching turbo frame -->
<%= turbo_frame_tag dom_id(@post, :edit) do %>
<%= form_with model: @post do |f| %>
<%= f.text_field :title %>
<%= f.text_area :content %>
<%= f.submit %>
<% end %>
<% end %>
<!-- Breaking out of frame navigation -->
<%= link_to "View all posts", posts_path,
data: { turbo_frame: "_top" } %>
<!-- Lazy loading frames -->
<%= turbo_frame_tag "recommendations",
src: recommendations_path,
loading: :lazy do %>
<p>Loading recommendations...</p>
<% end %>
Hotwire Turbo delivers SPA speed without JavaScript complexity. Turbo Drive accelerates navigation by replacing page body without full reload. Turbo Frames update page sections independently—click a frame link, only that frame refreshes. Turbo Streams push real-time updates from server—append, prepend, replace, remove, update actions. I combine Turbo with Rails—form submissions return Turbo Streams for dynamic UI updates. Turbo Native wraps web apps in native mobile shells. Progressive enhancement is key—apps work without JavaScript. Turbo reduces client-side JavaScript by ~90%, keeping logic server-side. Understanding Turbo Frame navigation rules is crucial—breaking out with data-turbo-frame="_top". Turbo makes reactive UIs accessible to Rails developers without React/Vue complexity.
Related snips
class CommentsController < ApplicationController
before_action :set_post
def create
@comment = @post.comments.build(comment_params)
System test: asserting Turbo Stream responses
class Money
include Comparable
attr_reader :amount, :currency
def initialize(amount, currency = 'USD')
Value objects for domain modeling
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
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
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
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
Share this code
Here's the card — post it anywhere.