DEV Community

Harsh patel
Harsh patel

Posted on

2 1 2 1

Modern Rails Rendering Techniques: Choosing Between Turbo Drive, Frames, Streams and Morph

In the evolving landscape of Ruby on Rails development, creating responsive, dynamic user interfaces without heavy JavaScript frameworks has become increasingly accessible. With the introduction of Hotwire (HTML Over The Wire) and subsequent innovations like Turbo Drive, Frames, Streams, and the newer Turbo Morph, developers now have multiple options for building interactive applications.

But which technique should you choose for your project? When should you use one over another? This guide will help you navigate these choices with practical examples.

Table of Contents

Understanding the Fundamentals

Before diving into specific techniques, let's understand the core philosophy: all these approaches aim to make web applications feel faster and more responsive by avoiding full page refreshes while keeping the development model server-centric.

Turbo Drive: The Foundation

What it is: Turbo Drive (formerly Turbolinks) accelerates navigation by converting traditional link clicks and form submissions into AJAX requests, replacing only the <body> of the page and merging the <head>.

When to use Turbo Drive:

  • For traditional multi-page applications where you want improved navigation speed
  • When you need minimal changes to existing code
  • As a foundation for other Turbo features

Example:

# No special code needed! Turbo Drive works automatically with:
<%= link_to "View Product", product_path(@product) %>

# For forms:
<%= form_with model: @product do |form| %>
  <%= form.text_field :name %>
  <%= form.submit %>
<% end %>
Enter fullscreen mode Exit fullscreen mode

With Turbo Drive, these standard Rails elements now deliver a smoother user experience without writing any JavaScript.

Live Demo:

Try clicking navigation links in this application - notice how the page transitions feel instant and there's no full page refresh.

Turbo Frames: Targeted Updates

What it is: Turbo Frames allow you to define independent sections of your page that can be updated independently of the rest of the content.

When to use Turbo Frames:

  • When you need to update a specific section of the page
  • For creating reusable components that load their own content
  • To implement master-detail interfaces
  • For forms that shouldn't navigate away from the current page

Example:

<%# app/views/products/index.html.erb %>
<h1>Products</h1>

<%= turbo_frame_tag "product_list" do %>
  <div class="products">
    <%= render @products %>
  </div>
<% end %>

<%= turbo_frame_tag "product_detail" %>
Enter fullscreen mode Exit fullscreen mode
<%# app/views/products/_product.html.erb %>
<div class="product">
  <%= link_to product.name, product, data: { turbo_frame: "product_detail" } %>
</div>
Enter fullscreen mode Exit fullscreen mode
<%# app/views/products/show.html.erb %>
<%= turbo_frame_tag "product_detail" do %>
  <div class="product-detail">
    <h2><%= @product.name %></h2>
    <p><%= @product.description %></p>
    <p>Price: $<%= @product.price %></p>
  </div>
<% end %>
Enter fullscreen mode Exit fullscreen mode

Live Demo:

Click on a product in the list, and only the detail panel updates, creating a smooth, app-like experience.

Turbo Streams: Real-time Updates

What it is: Turbo Streams allow the server to send HTML fragments that can add, replace, update, or remove elements on the page, often used with ActionCable for real-time updates.

When to use Turbo Streams:

  • For real-time features like chat, notifications, or collaborative editing
  • When you need to update multiple parts of the page simultaneously
  • For optimistic UI updates with form submissions

Example:

# app/controllers/comments_controller.rb
def create
  @comment = @post.comments.create!(comment_params)

  respond_to do |format|
    format.html { redirect_to @post }
    format.turbo_stream
  end
end
Enter fullscreen mode Exit fullscreen mode
<%# app/views/comments/create.turbo_stream.erb %>
<turbo-stream action="append" target="comments">
  <%= render partial: "comments/comment", locals: { comment: @comment } %>
</turbo-stream>

<turbo-stream action="update" target="new_comment">
  <%= render partial: "comments/form", locals: { post: @post, comment: Comment.new } %>
</turbo-stream>
Enter fullscreen mode Exit fullscreen mode

For real-time updates via ActionCable:

# app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :post

  after_create_commit -> { broadcast_append_to post }
end
Enter fullscreen mode Exit fullscreen mode
<%# app/views/posts/show.html.erb %>
<%= turbo_stream_from @post %>

<div id="comments">
  <%= render @post.comments %>
</div>
Enter fullscreen mode Exit fullscreen mode

Live Demo:

Try adding a comment below. Notice how it appears instantly without a page refresh, and if another user adds a comment, it will appear in real-time.

Turbo Morph: Fine-grained DOM Updates

What it is: The newest addition to the Turbo family, Turbo Morph provides more efficient DOM updates by diffing the current and new HTML to apply only the necessary changes.

When to use Turbo Morph:

  • For complex UI updates where standard Turbo frames or streams might be inefficient
  • When preserving UI state during updates is critical
  • For optimized rendering of large lists or complex components

Example:

<%# app/views/dashboard/show.html.erb %>
<%= turbo_frame_tag "dashboard", morph: true do %>
  <div class="dashboard">
    <div class="metrics">
      <%= render "metrics", data: @metrics %>
    </div>
    <div class="recent-activity">
      <%= render "activities", activities: @activities %>
    </div>
  </div>
<% end %>

<%= link_to "Refresh", dashboard_path, data: { turbo_frame: "dashboard" } %>
Enter fullscreen mode Exit fullscreen mode

In your controller:

# app/controllers/dashboard_controller.rb
def show
  @metrics = current_user.metrics
  @activities = current_user.recent_activities(25)
end
Enter fullscreen mode Exit fullscreen mode

Live Demo:

Click the "Refresh" button to update the dashboard. Notice how elements maintain their state (like scroll position or form inputs) even as the content changes.

Decision Framework: When to Use What

Here's a simple decision framework for choosing the right technique:

  1. Start with Turbo Drive as your baseline - it works automatically and improves navigation
  2. Use Turbo Frames when:
    • You need to update only a specific part of the page
    • You want to create master-detail interfaces
    • You're building isolated components
  3. Use Turbo Streams when:
    • You need real-time updates
    • You're updating multiple parts of the page at once
    • You're handling form submissions with complex responses
  4. Consider Turbo Morph when:
    • You're updating complex UI with many elements
    • You need to preserve DOM state during updates
    • You're working with large lists or tables

Real-world Example: Building a Comment System

Let's see how these technologies work together in a real-world example: a blog post with comments.

<%# app/views/posts/show.html.erb %>
<article class="post">
  <h1><%= @post.title %></h1>
  <div class="content">
    <%= @post.content %>
  </div>

  <h2>Comments (<span id="comment-count"><%= @post.comments.count %></span>)</h2>

  <%= turbo_stream_from @post, :comments %>

  <div id="comments" class="comments">
    <%= render @post.comments %>
  </div>

  <%= turbo_frame_tag "new_comment" do %>
    <%= render "comments/form", post: @post, comment: Comment.new %>
  <% end %>
</article>
Enter fullscreen mode Exit fullscreen mode
<%# app/views/comments/_form.html.erb %>
<%= form_with model: [post, comment], data: { controller: "reset-form" } do |form| %>
  <%= form.text_area :content, placeholder: "Add a comment..." %>
  <%= form.submit "Post" %>
<% end %>
Enter fullscreen mode Exit fullscreen mode
# app/controllers/comments_controller.rb
class CommentsController < ApplicationController
  before_action :set_post

  def create
    @comment = @post.comments.build(comment_params)
    @comment.user = current_user

    respond_to do |format|
      if @comment.save
        format.turbo_stream
      else
        format.turbo_stream { 
          render turbo_stream: turbo_stream.replace(
            "new_comment", 
            partial: "comments/form", 
            locals: { post: @post, comment: @comment }
          )
        }
      end
    end
  end

  private

  def set_post
    @post = Post.find(params[:post_id])
  end

  def comment_params
    params.require(:comment).permit(:content)
  end
end
Enter fullscreen mode Exit fullscreen mode
<%# app/views/comments/create.turbo_stream.erb %>
<turbo-stream action="append" target="comments">
  <%= render "comments/comment", comment: @comment %>
</turbo-stream>

<turbo-stream action="update" target="comment-count">
  <%= @post.comments.count %>
</turbo-stream>

<turbo-stream action="replace" target="new_comment">
  <%= render "comments/form", post: @post, comment: Comment.new %>
</turbo-stream>
Enter fullscreen mode Exit fullscreen mode
# app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :post
  belongs_to :user

  broadcasts_to ->(comment) { [comment.post, :comments] }, inserts_by: :append
end
Enter fullscreen mode Exit fullscreen mode

In this example:

  • Turbo Drive handles navigation to the post page
  • Turbo Frame manages the comment form submission
  • Turbo Streams handle real-time updates when new comments are added
  • The comment count updates in real-time

Performance Considerations

While these tools make building interactive applications easier, there are some performance considerations:

  1. Server Load: More frequent updates mean more server requests. Consider caching and pagination for high-traffic features.

  2. DOM Size: Large pages with many Turbo Frames can become memory-intensive. Use Turbo Morph for more efficient updates on complex pages.

  3. WebSocket Connections: For Turbo Streams with ActionCable, be mindful of connection limits in production environments.

  4. Progressive Enhancement: Always ensure your application works without JavaScript as a fallback.

Interactive Tutorial

Let's build a simple task manager to see these techniques in action:

  1. Create a new Rails application:
rails new taskmanager --css=tailwind
cd taskmanager
rails g scaffold Task name:string description:text completed:boolean
rails db:migrate
Enter fullscreen mode Exit fullscreen mode
  1. Update your routes:
# config/routes.rb
Rails.application.routes.draw do
  resources :tasks do
    member do
      patch :toggle
    end
  end
  root "tasks#index"
end
Enter fullscreen mode Exit fullscreen mode
  1. Update the controller:
# app/controllers/tasks_controller.rb
def toggle
  @task = Task.find(params[:id])
  @task.update(completed: !@task.completed)

  respond_to do |format|
    format.html { redirect_to tasks_path }
    format.turbo_stream
  end
end
Enter fullscreen mode Exit fullscreen mode
  1. Create the toggle view:
<%# app/views/tasks/toggle.turbo_stream.erb %>
<turbo-stream action="replace" target="<%= dom_id(@task) %>">
  <%= render partial: "task", locals: { task: @task } %>
</turbo-stream>

<turbo-stream action="update" target="task-count">
  <%= Task.count %> tasks, <%= Task.where(completed: true).count %> completed
</turbo-stream>
Enter fullscreen mode Exit fullscreen mode
  1. Update the task partial:
<%# app/views/tasks/_task.html.erb %>
<%= turbo_frame_tag dom_id(task) do %>
  <div class="task <%= task.completed? ? 'completed' : '' %>">
    <div class="flex items-center p-4 border-b">
      <%= link_to toggle_task_path(task), 
                  data: { turbo_method: :patch },
                  class: "mr-2" do %>
        <div class="w-6 h-6 border-2 rounded-full flex items-center justify-center <%= task.completed? ? 'bg-green-500 border-green-600' : 'border-gray-400' %>">
          <% if task.completed? %>
            <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor">
              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
            </svg>
          <% end %>
        </div>
      <% end %>

      <div class="<%= task.completed? ? 'line-through text-gray-500' : '' %>">
        <h3 class="font-medium"><%= task.name %></h3>
        <p class="text-sm text-gray-600"><%= task.description %></p>
      </div>

      <div class="ml-auto">
        <%= link_to "Edit", edit_task_path(task), class: "text-blue-500" %>
      </div>
    </div>
  </div>
<% end %>
Enter fullscreen mode Exit fullscreen mode
  1. Update the index view:
<%# app/views/tasks/index.html.erb %>
<div class="container mx-auto p-4">
  <h1 class="text-2xl font-bold mb-4">Task Manager</h1>

  <div id="task-count" class="mb-4">
    <%= Task.count %> tasks, <%= Task.where(completed: true).count %> completed
  </div>

  <div id="tasks">
    <%= render @tasks %>
  </div>

  <%= turbo_frame_tag "new_task", src: new_task_path, loading: "lazy" %>

  <div class="mt-4">
    <%= link_to "New Task", new_task_path, class: "px-4 py-2 bg-blue-500 text-white rounded", data: { turbo_frame: "new_task" } %>
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode
  1. Update the new task form:
<%# app/views/tasks/new.html.erb %>
<%= turbo_frame_tag "new_task" do %>
  <div class="bg-gray-100 p-4 rounded mb-4">
    <h2 class="text-xl font-semibold mb-2">New Task</h2>
    <%= render "form", task: @task %>

    <div class="mt-2">
      <%= link_to "Cancel", "#", class: "text-gray-500", data: { 
        controller: "turbo",
        action: "click->turbo#frameRemove" 
      } %>
    </div>
  </div>
<% end %>
Enter fullscreen mode Exit fullscreen mode

In this interactive example:

  • Turbo Drive handles navigation
  • Turbo Frames manage the task toggling and the new task form
  • Turbo Streams update the task count in real-time

Conclusion

The modern Rails stack gives developers powerful tools to create dynamic, responsive applications without relying on heavy JavaScript frameworks. By understanding when to use Turbo Drive, Frames, Streams, and Morph, you can build highly interactive experiences while maintaining the simplicity and productivity that makes Rails great.

Remember these key takeaways:

  1. Start simple with Turbo Drive, then add complexity as needed
  2. Choose the right tool for each interactive element in your application
  3. Combine techniques for the best user experience
  4. Consider performance implications for high-traffic applications

With these tools in your arsenal, you can build modern, responsive web applications that feel fast and reactive while maintaining the Rails philosophy of server-rendered HTML-first development.

What are you building with these techniques? Share your projects and challenges in the comments below!


Heroku

Deploy with ease. Manage efficiently. Scale faster.

Leave the infrastructure headaches to us, while you focus on pushing boundaries, realizing your vision, and making a lasting impression on your users.

Get Started

Top comments (0)

ACI image

ACI.dev: Fully Open-source AI Agent Tool-Use Infra (Composio Alternative)

100% open-source tool-use platform (backend, dev portal, integration library, SDK/MCP) that connects your AI agents to 600+ tools with multi-tenant auth, granular permissions, and access through direct function calling or a unified MCP server.

Check out our GitHub!