Hotwire Turbo for SPA-like user experiences

Sarah Mitchell Feb 2026
3 tabs
# 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
3 files · ruby, erb Explain with highlit

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.