Introduction

Building snappy, real‑time features in Rails used to mean hauling in hefty JavaScript frameworks. With Hotwire, Turbo and Stimulus, you can keep most behavior server‑side and let the browser handle updates via minimal JS. Turbo Frames handle targeted form submissions or link clicks, while Turbo Streams push live updates over WebSockets (via Action Cable) or long‑polling. This combo keeps your code clean and your app feeling instantaneous.

In this post, we’ll explore:

  • What Turbo Frames and Turbo Streams are and how they differ
  • Setting up server broadcasts with Action Cable
  • Using Stimulus controllers for extra polish
  • A simple collaborative to‑do list demo

Turbo Frames vs Turbo Streams

Turbo Frames

Think of a Turbo Frame as a mini‑page inside your page. Wrapping links or forms in <turbo-frame> tags means only that snippet reloads on navigation or submission, no full‑page refresh.

<turbo-frame id="user-details">
  <%= render 'users/details', user: @user %>
</turbo-frame>

<%= link_to 'Edit', edit_user_path(@user), data: { turbo_frame: 'user-details' } %>

When you click “Edit,” only the frame with ID user-details updates to show the edit form.

Turbo Streams

Turbo Streams send HTML fragments from the server to the client, specifying how to insert, update, or remove DOM elements. Under the hood, Rails serializes these fragments and sends them over Action Cable.

Example stream template (app/views/messages/create.turbo_stream.erb):

<turbo-stream action="append" target="messages">
  <template>
    <div class="message" id="message_<%= @message.id %>">
      <strong><%= @message.user.name %>:</strong> <%= @message.body %>
    </div>
  </template>
</turbo-stream>

Here, action="append" means add to the end of the #messages container. Other actions include prepend, replace, update, and remove.

Broadcasting with Action Cable

To broadcast Turbo Streams, set up a channel:

# app/channels/messages_channel.rb
class MessagesChannel < ApplicationCable::Channel
  def subscribed
    stream_for "chat_room_1"
  end
end

In your controller or model callback:

# app/controllers/messages_controller.rb
def create
  @message = Message.create!(message_params)
  MessagesChannel.broadcast_to(
    "chat_room_1",
    render_to_string(
      partial: 'messages/message',
      locals: { message: @message }
    )
  )
  head :ok
end

Or use Rails’ Turbo helper:

Turbo::StreamsChannel.broadcast_append_to(
  "chat_room_1",
  target: "messages",
  partial: "messages/message",
  locals: { message: @message }
)

On the client, subscribe via a small snippet in app/javascript/channels/messages_channel.js:

import consumer from "@rails/actioncable"

consumer.subscriptions.create("MessagesChannel", {
  connected() {},
  received(data) {
    document.getElementById('messages').insertAdjacentHTML('beforeend', data)
  }
})

With this in place, new messages show up live without you writing expensive frontend code.

Stimulus Controllers for Fine‑Grained Control

Stimulus is the lightweight JS layer in Hotwire, perfect for small behaviors. For example, auto‑scrolling the chat window when a new message arrives:

// app/javascript/controllers/auto_scroll_controller.js
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  connect() {
    this.scrollToBottom()
  }

  scrollToBottom() {
    this.element.scrollTop = this.element.scrollHeight
  }

  // Called by Turbo after content updates
  turboStreamAppended() {
    this.scrollToBottom()
  }
}

Attach it in your view:

<div id="messages" data-controller="auto-scroll"></div>

Now each time Turbo Streams appends a new message, the chat window stays scrolled to the bottom.

Mini‑Project: Collaborative To‑Do List

Let’s tie it together with a small example:

  1. Scaffold a Task model with title and completed booleans.
  2. Wrap your task list in a <turbo-frame id="tasks">.
  3. In TasksController#create, save the task then broadcast via broadcast_append_to.
  4. Create a Stimulus controller to focus the new‑task input after submission.

This yields a real‑time to‑do list where everyone on the page sees new tasks instantly.

Conclusion

Hotwire’s Turbo and Stimulus let you build real‑time Rails features with minimal JavaScript. By combining Frames for scoped updates, Streams for live broadcasts, and tiny Stimulus controllers for polish, you keep your application lean, maintainable, and lightning‑fast. Give it a try on your next Rails project, you might never pull in a heavy JS framework again!