Ruby on Rails has come a long way since its inception, and with the introduction of Hotwire and Turbo, building modern, real-time applications has never been easier. Gone are the days when you needed to write complex JavaScript to create dynamic, responsive web apps. Now, you can leverage the power of Rails and these cutting-edge tools to create lightning-fast, interactive experiences with minimal effort.
Let’s dive into the world of Hotwire and Turbo, and explore how they can revolutionize your Rails development process. Hotwire, short for HTML Over The Wire, is a set of technologies that allows you to build modern web applications without writing a single line of JavaScript. It’s all about sending HTML instead of JSON over the wire, hence the name.
At the heart of Hotwire is Turbo, a powerful library that enhances the performance and user experience of your Rails applications. Turbo is comprised of four main components: Turbo Drive, Turbo Frames, Turbo Streams, and Turbo Native. Each of these plays a crucial role in creating seamless, app-like experiences in your web applications.
Turbo Drive is the foundation of Hotwire. It accelerates links and form submissions by negating the need for full page reloads. Instead, it intercepts clicks on links and form submissions, fetches the new content via AJAX, and updates the page’s body without a full refresh. This results in a much smoother user experience, reminiscent of a single-page application (SPA), but without the complexity of managing a frontend JavaScript framework.
To get started with Turbo Drive, you don’t need to do much. If you’re using Rails 7 or later, it’s included by default. Otherwise, you can add it to your Gemfile:
gem 'turbo-rails'
Then, run bundle install
and restart your server. That’s it! Turbo Drive is now active on your site, and you’ll notice an immediate improvement in navigation speed.
Next up is Turbo Frames. This feature allows you to update specific parts of your page without touching the rest. It’s perfect for creating independent sections that can be updated independently, like a comments section or a live-updating list.
To use Turbo Frames, wrap the content you want to update in a turbo_frame_tag
:
<%= turbo_frame_tag "comments" do %>
<%= render @comments %>
<% end %>
Now, when you submit a form within this frame or click a link targeting it, only the content within the frame will be updated. It’s like having multiple mini-applications within your main app!
Turbo Streams takes things a step further by allowing you to send HTML fragments over WebSocket connections. This enables real-time updates without any page reloads or frame refreshes. It’s perfect for live notifications, chat applications, or any feature that requires instant updates.
Here’s a simple example of a Turbo Stream:
turbo_stream.append "messages", partial: "messages/message", locals: { message: @message }
This code would append a new message to a list of messages in real-time, without any page reload.
Lastly, Turbo Native brings all these capabilities to native iOS and Android apps. It allows you to wrap your Rails app in a native shell, giving you the best of both worlds: the ease of web development with the performance and features of native apps.
Now that we’ve covered the basics, let’s dive into a more complex example. Imagine we’re building a real-time chat application using Rails and Hotwire. Here’s how we might structure our messages controller:
class MessagesController < ApplicationController
def create
@message = current_user.messages.create!(message_params)
respond_to do |format|
format.turbo_stream
end
end
private
def message_params
params.require(:message).permit(:content)
end
end
In our view, we’d have a Turbo Frame for the messages:
<%= turbo_frame_tag "messages" do %>
<%= render @messages %>
<% end %>
<%= form_with(model: @message, local: false) do |f| %>
<%= f.text_field :content %>
<%= f.submit "Send" %>
<% end %>
And in our create.turbo_stream.erb
:
<%= turbo_stream.append "messages", @message %>
With just this code, we’ve created a real-time chat application. When a user submits the form, the new message is instantly appended to the messages list for all connected users, without any page reloads or explicit JavaScript.
But what if we want to go further? Let’s add a typing indicator. We can use Turbo Streams to broadcast when a user starts and stops typing. First, we’ll need to add a new action to our controller:
def typing
Turbo::StreamsChannel.broadcast_update_to(
"chat_room",
target: "typing_indicator",
partial: "messages/typing_indicator",
locals: { user: current_user }
)
end
Then, in our JavaScript, we can send a request to this action when the user starts typing:
document.addEventListener('turbo:load', () => {
const input = document.querySelector('#message_content')
let typingTimer
input.addEventListener('keydown', () => {
clearTimeout(typingTimer)
fetch('/messages/typing', { method: 'POST' })
typingTimer = setTimeout(() => {
fetch('/messages/stopped_typing', { method: 'POST' })
}, 1000)
})
})
This code sends a POST request to our typing
action when the user starts typing, and another to a stopped_typing
action (which we’d need to implement) when they stop.
The beauty of Hotwire is that it allows us to create these interactive, real-time features with minimal JavaScript. Most of our logic remains on the server, written in Ruby, which many Rails developers find more comfortable and maintainable.
But Hotwire isn’t just for chat apps. It’s incredibly versatile. Consider an e-commerce site where you want to update the cart in real-time as users add or remove items. With Turbo Streams, this becomes trivial. You could have a Turbo Frame for the cart:
<%= turbo_frame_tag "cart" do %>
<%= render "cart" %>
<% end %>
Then, in your add_to_cart
action:
def add_to_cart
# Add item to cart logic here
respond_to do |format|
format.turbo_stream do
render turbo_stream: turbo_stream.replace("cart", partial: "cart")
end
end
end
Now, whenever a user adds an item to their cart, it will update in real-time for them, creating a smooth, app-like experience.
One of the most powerful aspects of Hotwire is how it allows you to progressively enhance your application. You can start with a traditional Rails app and gradually add Turbo features as needed. This makes it an excellent choice for both new projects and for modernizing existing applications.
However, it’s important to note that while Hotwire can dramatically reduce the amount of JavaScript you need to write, it doesn’t eliminate the need for JavaScript entirely. There will still be cases where custom JavaScript is necessary for complex interactions or animations. The key is that Hotwire handles the heavy lifting of DOM manipulation and server communication, allowing you to focus on application-specific logic when you do need to write JavaScript.
As you dive deeper into Hotwire and Turbo, you’ll discover more advanced techniques. For example, you can use Turbo Streams to update multiple elements on a page simultaneously. Let’s say you have a blog post with comments and a comment count. When a new comment is added, you might want to update both the comments list and the count:
def create
@comment = @post.comments.create!(comment_params)
respond_to do |format|
format.turbo_stream do
render turbo_stream: [
turbo_stream.append("comments", @comment),
turbo_stream.replace("comment_count", @post.comments.count)
]
end
end
end
This will update both elements in a single response, providing a cohesive update to the user.
Another powerful feature is the ability to combine Turbo with Stimulus, another part of the Hotwire suite. Stimulus allows you to add sprinkles of JavaScript behavior to your HTML. For instance, you could use Stimulus to add a character count to a textarea:
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [ "input", "count" ]
connect() {
this.update()
}
update() {
this.countTarget.textContent = this.inputTarget.value.length
}
}
Then in your HTML:
<div data-controller="character-count">
<%= form.text_area :content, data: { character_count_target: "input", action: "input->character-count#update" } %>
<p>Character count: <span data-character-count-target="count"></span></p>
</div>
This creates a dynamic character count that updates as the user types, all without leaving your Rails view.
As you can see, Hotwire and Turbo open up a world of possibilities for creating dynamic, responsive Rails applications with minimal JavaScript. They allow you to leverage your existing Rails knowledge to create modern, app-like experiences that users love.
But like any tool, Hotwire isn’t a silver bullet. It’s important to understand its limitations and know when to reach for other tools. For highly complex, state-heavy applications, a JavaScript framework like React or Vue might still be a better fit. And for applications that need to work offline or have complex client-side logic, you might need to combine Hotwire with more traditional JavaScript approaches.
In my experience, Hotwire shines brightest in applications that are primarily server-rendered but need sprinkles of interactivity. It’s perfect for adding real-time updates to otherwise traditional Rails apps, or for building admin interfaces that need to be snappy and responsive.
As you explore Hotwire and Turbo, don’t be afraid to experiment. Try refactoring parts of your existing applications to use Turbo Frames or Streams. You might be surprised at how much you can improve the user experience with just a few lines of code.
Remember, the goal of Hotwire isn’t to completely eliminate JavaScript, but to reduce its footprint and complexity in your Rails applications. By sending HTML over the wire instead of JSON, and by leveraging the power of your server, you can create faster, more maintainable applications that are a joy to develop and use.
So go forth and explore the world of Hotwire and Turbo. Your Rails applications (and your users) will thank you for it. Happy coding!