Rails Hotwire
Turbo Drive
Turbo Frames
Turbo Streams
Turbo & Stimulus Together
Performance, Caching & SEO
Technical Question & Answer
Detailed Explanation
Hotwire helps you build websites that feel fast and modern — without writing a lot of JavaScript.
Normally, websites use JavaScript to get data and build pages in the browser. But with Hotwire, your server sends the final HTML, and the page updates automatically — no reload, no flicker.
It’s like the server says: “Here’s the exact thing you need,” and the browser just shows it.
Hotwire has three tools that work together:
- Turbo Drive: Makes page loads super fast (no reloads)
- Turbo Frames: Updates just a part of the page (not the whole thing)
- Turbo Streams: Updates your page in real time (like live chat)
You can also use Stimulus to add small JavaScript features like modals or toggles.
Code Example
<%= turbo_frame_tag "comments" do %>
<%= render @comments %>
<% end %>
# Turbo Stream example
<%= turbo_stream.append "comments" do %>
<div class="comment"><%= @comment.body %></div>
<% end %>
Common Problems and Solutions
🛑 Problem 1: Turbo Stream not updating the DOM
You’ve set up a Turbo Stream broadcast, but the DOM isn’t updating on the client.
✅ Solution:Make sure you’re targeting the correct DOM ID in the stream and that it exists in the HTML.
# Correct DOM target
<div id="comments">
<%= turbo_stream_from "comments" %>
</div>
# Turbo Stream Partial
<%= turbo_stream.append "comments" do %>
<%= render @comment %>
<% end %>
🛑 Problem 2: Stimulus controller not triggering after Turbo updates
A Stimulus controller is attached to an element, but stops working after Turbo updates the element.
✅ Solution:Stimulus automatically reinitializes, but make sure your controller is attached to the element being re-rendered, not removed.
# Good: Stimulus controller lives inside turbo-frame
<turbo-frame id="profile">
<div data-controller="profile">
<button data-action="click->profile#edit">Edit</button>
</div>
</turbo-frame>
🛑 Problem 3: Turbo frame redirects cause full-page reload
You’re expecting a Turbo Frame to update only a portion of the page, but a redirect causes the whole page to reload.
✅ Solution:Turbo Frames don’t persist across redirects unless you use `turbo_stream` format or a `turbo_frame_request?` check.
# In controller
if turbo_frame_request?
render partial: "form", locals: { post: @post }
else
redirect_to posts_path
end
🛑 Problem 4: Turbo frame not replacing target correctly
When submitting a form inside a Turbo Frame, the response does not replace the frame as expected.
✅ Solution:Ensure your response is wrapped in the same Turbo Frame ID and that the response is rendered with `turbo_stream` or directly within the frame tag.
# Form
<turbo-frame id="new_comment">
<%= form_with model: @comment, data: { turbo_frame: "new_comment" } do |f| %>
<%= f.text_area :content %>
<% end %>
</turbo-frame>
# Controller Response (no redirect)
render partial: "form", locals: { comment: @comment }
🛑 Problem 5: Broadcasted updates not reaching all clients
You’ve set up Turbo Streams with ActionCable, but other users are not seeing real-time updates.
✅ Solution:Ensure you’ve correctly subscribed to a Turbo Stream channel and you’re broadcasting to the same stream name.
# View
<%= turbo_stream_from "notifications" %>
# Broadcast (Rails)
Turbo::StreamsChannel.broadcast_append_to(
"notifications",
target: "notifications",
partial: "notifications/notification",
locals: { notification: @notification }
)
Real-World Scenario
Imagine a blog platform where users can comment. Using Hotwire, new comments appear instantly using Turbo Streams without reloading the page or writing custom JavaScript.
In a chat app, when a new message is created, it gets broadcasted to all users in real time using Turbo Streams. The front-end HTML is updated without writing any JavaScript or using frontend frameworks like React or Vue.
Alternatives
- React/Vue with traditional JSON APIs
- StimulusReflex for real-time interactions
- LiveView (Elixir/Phoenix)
Best Practices
✅ Use Consistent Turbo Frame IDs
Make sure the frame IDs used in your forms and responses match exactly. Turbo will only replace content if the IDs match.
# In View
<turbo-frame id="post_form">
<%= render "form", post: @post %>
</turbo-frame>
# In Controller
render partial: "form", locals: { post: @post }
✅ Use Turbo Streams for Real-Time UI Updates
Turbo Streams are ideal for real-time updates when new records are created, updated, or deleted.
# View subscription
<%= turbo_stream_from "comments" %>
# Stream update
<%= turbo_stream.append "comments" do %>
<%= render @comment %>
<% end %>
✅ Use Turbo Frames for Partial Page Updates
Wrap sections of your UI that you want to replace without a full-page reload.
<turbo-frame id="profile_details">
<p><%= @user.name %></p>
<p><%= @user.email %></p>
</turbo-frame>
✅ Avoid Nested Turbo Frames (unless necessary)
Nesting frames can cause unpredictable updates. Instead, flatten your structure or break it into smaller components.
# ❌ Avoid:
<turbo-frame id="outer">
<turbo-frame id="inner">...</turbo-frame>
</turbo-frame>
# ✅ Better:
<turbo-frame id="outer">...</turbo-frame>
<turbo-frame id="inner">...</turbo-frame>
✅ Use Stimulus to Handle Interactive UI Behavior
Turbo handles HTML updates. Use Stimulus to add JS-based interactivity (e.g., dropdowns, modals, toggles).
# HTML
<div data-controller="toggle">
<button data-action="click->toggle#toggle">Show/Hide</button>
<div data-toggle-target="content" style="display:none;">More Info</div>
</div>
# toggle_controller.js
export default class extends Controller {
static targets = ["content"]
toggle() {
this.contentTarget.hidden = !this.contentTarget.hidden;
}
}
✅ Use Background Jobs for Broadcasts
When broadcasting Turbo Streams, use background jobs to keep your controllers fast and scalable.
# app/jobs/comment_broadcast_job.rb
class CommentBroadcastJob < ApplicationJob
def perform(comment)
Turbo::StreamsChannel.broadcast_append_to(
"comments",
target: "comments",
partial: "comments/comment",
locals: { comment: comment }
)
end
end
✅ Optimize for Turbo Drive Navigation
Turbo Drive enhances performance by intercepting page loads. Avoid including conflicting JS in `
`.# In layout.html.erb
<head>
<meta name="turbo-cache-control" content="no-preview">
<%= javascript_include_tag "application", "data-turbo-track": "reload" %>
</head>
Detailed Explanation
Hotwire is a server-driven approach where HTML is sent from the server and dynamically updated on the client. In contrast, Single Page Applications (SPAs) like React or Vue fetch JSON data and render UI components on the client side. Hotwire reduces the amount of custom JavaScript by leveraging server-rendered HTML with Turbo (for navigation and updates) and Stimulus (for interactivity).
Code Example
// Hotwire - Turbo Stream Example (Server-side rendering)
<%= turbo_stream.append "messages" do %>
<div class="message"><%= @message.content %></div>
<% end %>
// React SPA - Client-side rendering
fetch("/api/messages")
.then(res => res.json())
.then(data => setMessages(data));
Common Questions, Problems & Solutions
❓ Q1: Can Hotwire replace a full JavaScript SPA like React or Vue?
Problem: Developers want to migrate full SPA logic to Hotwire but expect React-like behavior.
Solution: Hotwire is best for CRUD and real-time updates where server-rendered HTML suffices. Avoid complex frontend state management in Hotwire.
// ❌ Complex SPA-style state in JavaScript
setState(prev => ({ ...prev, comments: updated }));
// ✅ Hotwire approach
<%= turbo_stream.append "comments" do %>
<%= render @comment %>
<% end %>
❓ Q2: Why does clicking a link in Turbo sometimes reload the page?
Problem: Turbo Drive replaces the entire page when visiting non-Turbo-enabled paths or external links.
Solution: Use `data-turbo=”false”` on external links or disable Turbo for routes that conflict.
<a href="/external" data-turbo="false">Open External Page</a>
❓ Q3: Can I fetch partial data and update part of the DOM like React?
Problem: In SPAs, you fetch JSON and render the result in components. In Hotwire, the goal is to send HTML.
Solution: Wrap partial content in `turbo_frame_tag`, render the partial on the server, and replace only that section.
# Frame in HTML
<turbo-frame id="profile"></turbo-frame>
# Controller
render partial: "users/profile", locals: { user: @user }
❓ Q4: Why does Turbo stream fail to update when broadcasting?
Problem: Turbo Stream broadcasts silently fail due to missing subscriptions or mismatched IDs.
Solution: Ensure all clients subscribe to the same stream and the stream includes the target DOM ID.
# View
<%= turbo_stream_from "posts" %>
# Broadcast
Turbo::StreamsChannel.broadcast_append_to(
"posts",
target: "posts",
partial: "posts/post",
locals: { post: @post }
)
❓ Q5: How do I handle interactivity without React?
Problem: React handles modals, dropdowns, and toggles easily. In Hotwire, we still need some JavaScript.
Solution: Use Stimulus.js — a lightweight JavaScript framework built for Hotwire.
// HTML
<div data-controller="modal">
<button data-action="click->modal#open">Open Modal</button>
<div data-modal-target="box" hidden>Modal content</div>
</div>
// modal_controller.js
export default class extends Controller {
static targets = ["box"]
open() {
this.boxTarget.hidden = false;
}
}
Real-World Scenario
In Basecamp, Hotwire allows dynamic updates (new messages, notifications) without writing React or managing a frontend state. A new comment is broadcasted to all users as an HTML fragment.
Alternative Methods
- React or Vue SPA with REST or GraphQL APIs
- Phoenix LiveView (Elixir)
- StimulusReflex (Rails)
Best Practices with Examples
✅ 1. Use Turbo Frames for Partial Page Updates
Instead of rendering entire pages, wrap sections of your UI in `turbo-frame` tags. This allows only that section to be updated on form submission or navigation.
# View
<turbo-frame id="user_form">
<%= form_with model: @user do |f| %>
<%= f.text_field :name %>
<%= f.submit %>
<% end %>
</turbo-frame>
# Controller
render partial: "form", locals: { user: @user }
✅ 2. Use Unique and Predictable DOM IDs
Turbo Streams rely on DOM ID targets. Always ensure your targets (like list IDs or form wrappers) are unique and predictable.
# List wrapper
<div id="comments">
<%= turbo_stream_from "comments" %>
</div>
# Broadcast
Turbo::StreamsChannel.broadcast_append_to(
"comments",
target: "comments",
partial: "comments/comment",
locals: { comment: @comment }
)
✅ 3. Offload Turbo Stream Broadcasting to Background Jobs
Don’t block user-facing requests with broadcast logic. Instead, push to a background job and broadcast from there.
# Job
class CommentBroadcastJob < ApplicationJob
def perform(comment)
Turbo::StreamsChannel.broadcast_append_to(
"comments",
target: "comments",
partial: "comments/comment",
locals: { comment: comment }
)
end
end
# Controller
CommentBroadcastJob.perform_later(@comment)
✅ 4. Use Stimulus for UI Behavior, not Full Logic
Stimulus works great for light UI behavior — like toggles, modals, or animation triggers — and keeps your code clean.
# HTML
<div data-controller="dropdown">
<button data-action="click->dropdown#toggle">Menu</button>
<div data-dropdown-target="menu" hidden>Options here</div>
</div>
# dropdown_controller.js
export default class extends Controller {
static targets = ["menu"]
toggle() {
this.menuTarget.hidden = !this.menuTarget.hidden;
}
}
✅ 5. Avoid Forcing SPA Behavior in a Hotwire App
Don’t try to mimic complex SPA state management in Hotwire. Embrace its server-driven rendering and minimal JavaScript philosophy.
# ❌ Don’t:
useState([...comments]); // SPA-style state
# ✅ Do:
Broadcast HTML using Turbo Streams and update with Turbo Frames
Detailed Explanation
Hotwire is a full-stack framework for building modern web applications without using much custom JavaScript. It consists of Turbo (for real-time DOM updates and navigation) and Stimulus (for light JavaScript interactions). To enable it in a new Rails app, you simply run the built-in installer which configures everything for you.
Code Example
# Step 1: Create a new Rails app (or use existing)
rails new myapp --javascript esbuild
# Step 2: Run the Hotwire installer
bin/rails hotwire:install
# This will:
# ✅ Install Turbo & Stimulus
# ✅ Add to importmap or JS bundler
# ✅ Create controllers directory
# ✅ Include Turbo-rails in Gemfile
Common Questions, Problems & Solutions
❓ Q1: Why is Turbo not working after running hotwire:install
?
Problem: After installation, Turbo navigation doesn’t work — links still perform full-page reloads.
Solution: Ensure Turbo is properly imported and initialized in your JavaScript entry file.
// In app/javascript/application.js or packs/application.js
import "@hotwired/turbo-rails"
❓ Q2: Why aren’t my Stimulus controllers being recognized?
Problem: You’ve created a controller in `controllers/` but actions don’t trigger.
Solution: Make sure the controller is properly registered in `controllers/index.js`.
// controllers/index.js
import { Application } from "@hotwired/stimulus"
import HelloController from "./hello_controller"
const application = Application.start()
application.register("hello", HelloController)
❓ Q3: My app doesn’t recognize Turbo Stream responses
Problem: Turbo Stream updates are not working — the response is ignored.
Solution: Ensure your controller action renders a turbo_stream response format.
# In your controller
respond_to do |format|
format.turbo_stream
format.html
end
❓ Q4: How do I verify that Hotwire is working correctly after install?
Problem: You’re unsure if Hotwire is installed and active.
Solution: Use `console.log` in Stimulus or inspect Turbo navigation events.
// Add to your Stimulus controller
connect() {
console.log("Stimulus connected")
}
// Add Turbo event listener
document.addEventListener("turbo:load", () => {
console.log("Turbo is working!")
})
❓ Q5: Why are broadcasts not working after setting up Turbo Streams?
Problem: Turbo Stream broadcasts are not reaching clients.
Solution: Ensure the client has subscribed with `turbo_stream_from`, and you’re broadcasting to the correct stream.
# View
<%= turbo_stream_from "comments" %>
# Controller or job
Turbo::StreamsChannel.broadcast_append_to(
"comments",
target: "comments",
partial: "comments/comment",
locals: { comment: @comment }
)
Real-World Scenario
A developer starts a new blog platform. Instead of using React for dynamic post submissions, they install Hotwire. Now form submissions automatically update the DOM using Turbo Streams without writing any frontend JS.
Alternative Methods or Concepts
- Install manually via CDN instead of Rails installer
- Use importmap-rails instead of ESBuild if preferred
- Use Turbo + Stimulus in a non-Rails stack via CDN
Best Practices with Examples
✅ 1. Use the Built-in Installer Instead of Manual Setup
The Rails command bin/rails hotwire:install
handles all the wiring: adds Turbo, Stimulus, creates controller folders, and updates JS packs. Avoid manual steps unless necessary.
# Terminal
bin/rails hotwire:install
✅ 2. Confirm Turbo and Stimulus are Properly Imported
After installation, ensure that your JavaScript entry file (like application.js
) imports Turbo and Stimulus.
// app/javascript/application.js
import "@hotwired/turbo-rails"
import "./controllers"
✅ 3. Keep Stimulus Controllers Organized
Place all Stimulus controllers inside the app/javascript/controllers
folder. Use one controller per feature to stay modular.
// Example directory
controllers/
dropdown_controller.js
modal_controller.js
navbar_controller.js
✅ 4. Use Turbo Frames for Form and Section Isolation
From the start, wrap your form and updateable components in <turbo-frame>
. This enables smooth updates without full page reloads.
<turbo-frame id="comment_form">
<%= render "form" %>
</turbo-frame>
✅ 5. Use Dev Console to Debug Install Issues
Add logs to check whether Turbo and Stimulus are running. For example, log turbo:load
and Stimulus controller connections.
// app/javascript/controllers/hello_controller.js
connect() {
console.log("Stimulus connected!")
}
// Somewhere in JS
document.addEventListener("turbo:load", () => {
console.log("Turbo is active!")
})
✅ 6. Commit Install-Generated Files Immediately
Hotwire installation generates multiple files (controllers, packs, initializers). It’s a good practice to commit them right away for easier rollbacks and collaboration.
# Good Git commit message:
"Install Hotwire (Turbo + Stimulus) using built-in installer"
Detailed Explanation
Turbo is made up of three key components:
-
Turbo Drive:
Turbo Drive makes your website feel really fast by changing pages without doing a full reload.
When you click a link or submit a form, it loads only the new content and keeps things like the layout, navigation bar, or footer in place.
This saves time and gives a smooth, app-like feel.
Example: You click “About Us” — instead of reloading the whole page, Turbo Drive just swaps the main content section. -
Turbo Frames:
Turbo Frames let you update only a part of the page — like a form, a profile section, or a list — without touching the rest of the page.
You wrap that section in a special tag, and when it changes, Turbo updates only that part.
Example: You submit a comment form, and only the form area updates to show “Thanks for commenting” — the rest of the page stays exactly the same. -
Turbo Streams:
Turbo Streams update your page automatically in real time when something new happens.
This is great for live features like chat apps, notification panels, or live comment feeds.
The server sends a small HTML snippet and Turbo inserts it in the right place — no reload, no delay.
Example: Another user sends a message in a chat — it appears on your screen instantly without refreshing the page.
Code Example
// Turbo Drive (automatic)
<a href="/posts">All Posts</a>
// Turbo Frame
<turbo-frame id="form_frame">
<%= form_with model: @comment %>
</turbo-frame>
// Turbo Stream (server broadcast)
<%= turbo_stream.append "comments" do %>
<%= render @comment %>
<% end %>
Common Questions, Problems & Solutions
❓ Q1: Why does Turbo Drive reload the entire page instead of updating it seamlessly?
Problem: A link or form submission results in a full page reload.
Solution: Check if you’ve disabled Turbo unintentionally using data-turbo="false"
or using an unsupported HTTP method like PUT/DELETE without correct setup.
// ✅ Default behavior
<a href="/posts">View Posts</a>
// ❌ Turbo disabled
<a href="/posts" data-turbo="false">View Posts</a>
❓ Q2: Why isn’t my Turbo Frame updating after submitting a form?
Problem: The frame doesn’t change after submitting a form inside it.
Solution: Ensure your server response includes a Turbo Frame with the same ID as the one wrapping the form.
// View
<turbo-frame id="comment_form">
<%= form_with model: @comment, data: { turbo_frame: "comment_form" } do |f| %>
...
<% end %>
</turbo-frame>
// Response Partial
<turbo-frame id="comment_form">
<p>Thanks for commenting!</p>
</turbo-frame>
❓ Q3: Why is my Turbo Stream broadcast not appearing on the client side?
Problem: A Turbo Stream append or replace action isn’t visible to users.
Solution: Confirm both sides are subscribed to the same stream name using turbo_stream_from
.
// View
<div id="notifications">
<%= turbo_stream_from "notifications" %>
</div>
// Broadcast
Turbo::StreamsChannel.broadcast_append_to(
"notifications",
target: "notifications",
partial: "notifications/notification",
locals: { notification: @notification }
)
❓ Q4: Can I use Turbo Frames and Turbo Streams together?
Problem: Confusion about when and how to combine both.
Solution: Yes, use Turbo Frames for scoped updates and Turbo Streams for real-time global updates.
// Turbo Frame (form update in place)
<turbo-frame id="form_section">...</turbo-frame>
// Turbo Stream (real-time feed)
<%= turbo_stream.append "feed" do %>
<%= render @post %>
<% end %>
❓ Q5: How do I debug issues with Turbo events?
Problem: It’s unclear why navigation or frames don’t behave as expected.
Solution: Use browser console logs for Turbo events such as turbo:load
, turbo:frame-load
, etc.
// Add to JavaScript
document.addEventListener("turbo:load", () => {
console.log("Turbo Drive loaded");
});
document.addEventListener("turbo:frame-load", (e) => {
console.log("Frame loaded: ", e.target.id);
});
Real-World Scenario
In a forum app, Turbo Drive loads new thread pages seamlessly. Inside the thread, replies are submitted within a Turbo Frame to avoid reloading the entire thread. New replies appear in real-time using Turbo Streams.
Alternative Methods or Concepts
- Traditional Rails full-page reloads
- React/Vue with JSON APIs for dynamic rendering
- StimulusReflex for reactive UI
Best Practices with Examples
✅ 1. Use Turbo Drive for Seamless Page Navigation
Turbo Drive enhances normal link and form behavior across your entire Rails app. It should be enabled globally unless explicitly disabled.
// app/views/layouts/application.html.erb
<head>
<meta name="turbo-cache-control" content="no-preview">
<%= javascript_include_tag "application", "data-turbo-track": "reload", defer: true %>
</head>
<a href="/posts">All Posts</a> <!-- Loads via Turbo Drive -->
✅ 2. Wrap Dynamic Sections with Turbo Frames
Isolate updateable sections using <turbo-frame>
. The server should respond with a partial wrapped in the same frame ID.
// View
<turbo-frame id="edit_post">
<%= render "form", post: @post %>
</turbo-frame>
// Controller response (partial)
<turbo-frame id="edit_post">
<p>Post updated successfully!</p>
</turbo-frame>
✅ 3. Use Turbo Streams for Real-Time, Global Updates
Turbo Streams are ideal for updating content across multiple sessions or tabs in real-time. Use them with ActionCable or background jobs.
// View
<div id="comments">
<%= turbo_stream_from "comments" %>
</div>
// Broadcast
Turbo::StreamsChannel.broadcast_append_to(
"comments",
target: "comments",
partial: "comments/comment",
locals: { comment: @comment }
)
✅ 4. Combine Frames + Streams Smartly
Use Turbo Frames for user-triggered interactions (e.g., form submissions), and Turbo Streams for server-triggered updates (e.g., new data from other users).
// Frame: Add comment form
<turbo-frame id="new_comment">
<%= render "form" %>
</turbo-frame>
// Stream: Append new comment
<%= turbo_stream.append "comments" do %>
<%= render @comment %>
<% end %>
✅ 5. Always Match Turbo Frame IDs Between View and Response
If the Turbo Frame response doesn’t include the same id
as the request, it won’t be replaced. Keep them identical.
// ✅ Correct usage
<turbo-frame id="modal">
<%= render "modal_content" %>
</turbo-frame>
// Response should also contain:
<turbo-frame id="modal">Updated content</turbo-frame>
Detailed Explanation
Turbo Drive listens for clicks on links and submissions on forms. Instead of doing a full-page reload, it fetches the new page using AJAX and replaces the <body>
content, making navigation feel seamless and instant.
It also preserves page state like scroll position and browser history.
Code Example
// Normal link in Rails
<a href="/posts">All Posts</a>
// Turbo Drive intercepts this click and loads content via AJAX
// Normal form
<%= form_with model: @post %>
<%= f.text_field :title %>
<%= f.submit "Save" %>
<% end %>
// Turbo intercepts the form and replaces the body with the server response
Common Questions, Problems & Solutions
❓ Q1: Why isn’t Turbo Drive intercepting my link clicks?
Problem: Clicking links still reloads the whole page instead of behaving like a single-page app.
Solution: Make sure you’re not disabling Turbo on the link, and that you’re using a standard anchor tag without target="_blank"
or data-turbo="false"
.
// ✅ Turbo-enabled
<a href="/posts">Posts</a>
// ❌ Turbo disabled
<a href="/posts" data-turbo="false">Posts</a>
❓ Q2: My form submits, but the whole page reloads. Why?
Problem: Turbo isn’t intercepting the form submission as expected.
Solution: Use form_with
(which is remote by default in Rails 7+) and make sure it’s not using a local: true
option.
// ✅ Turbo-compatible
<%= form_with model: @post do |f| %>
<%= f.text_field :title %>
<%= f.submit %>
<% end %>
// ❌ Local-only form (disables Turbo)
<%= form_with model: @post, local: true do |f| %>
...
<% end %>
❓ Q3: How do I stop Turbo Drive from handling a specific link?
Problem: You want a link to behave normally (like opening in a new tab or skipping Turbo behavior).
Solution: Add data-turbo="false"
or use target="_blank"
.
// Open in new tab (bypasses Turbo)
<a href="/external" target="_blank">External Site</a>
// Disable Turbo on this link
<a href="/logout" data-turbo="false">Log Out</a>
❓ Q4: Why does my JavaScript stop working after Turbo navigation?
Problem: JavaScript code that runs on page load doesn’t work after Turbo loads a new page.
Solution: Listen to the turbo:load
event instead of DOMContentLoaded
.
// ✅ Works with Turbo
document.addEventListener("turbo:load", () => {
console.log("Turbo page loaded!");
});
// ❌ Only runs on full reload
document.addEventListener("DOMContentLoaded", () => {
console.log("Loaded!");
});
❓ Q5: How do I track Turbo navigation or debug what it’s doing?
Problem: You want to confirm that Turbo Drive is intercepting clicks and working correctly.
Solution: Add event listeners in JavaScript or inspect the Network tab — Turbo sends fetch requests instead of full-page reloads.
// Log Turbo navigation events
document.addEventListener("turbo:visit", (e) => {
console.log("Visiting:", e.detail.url);
});
document.addEventListener("turbo:load", () => {
console.log("New content loaded by Turbo!");
});
Real-World Scenario
In a blog app, when a user clicks from the homepage to a post detail page, Turbo Drive swaps the main content instead of reloading the full page. This feels smoother and faster, like using a single-page app.
Alternative Methods or Concepts
- Traditional full-page reloads using Rails
link_to
- Single Page Applications using React or Vue
- Using PJAX or Turbolinks (older solutions)
Best Practices with Examples
✅ 1. Use Standard Links and Forms
Always use regular <a>
tags for links and form_with
for forms. Turbo Drive automatically handles them without extra setup.
// ✅ Link
<a href="/posts">View Posts</a>
// ✅ Turbo-friendly form
<%= form_with model: @post do |f| %>
<%= f.text_field :title %>
<%= f.submit "Save" %>
<% end %>
✅ 2. Avoid data-turbo="false"
Unless Necessary
If you add data-turbo="false"
to a link or form, Turbo won’t intercept it. Only use this if you really want full-page reload behavior.
// ✅ Skip Turbo only for logout or special cases
<a href="/logout" data-turbo="false">Log out</a>
✅ 3. Listen for turbo:load
Instead of DOMContentLoaded
If you’re running custom JavaScript after page load, make sure it’s inside a turbo:load
listener. Turbo Drive doesn’t trigger DOMContentLoaded
on navigation.
// ✅ Works with Turbo
document.addEventListener("turbo:load", () => {
console.log("Turbo page loaded");
});
✅ 4. Use Stimulus for Re-initializing UI Components
If you need interactivity like modals, dropdowns, or tooltips, use Stimulus controllers so they auto-reinitialize after Turbo page updates.
// Example controller
export default class extends Controller {
connect() {
console.log("Controller reconnected");
}
}
✅ 5. Use Turbo Events for Debugging and UX Enhancements
Listen to Turbo events like turbo:before-fetch-request
and turbo:load
to monitor navigation or add loading indicators.
// Add a loader during page fetch
document.addEventListener("turbo:before-fetch-request", () => {
document.body.classList.add("loading");
});
document.addEventListener("turbo:load", () => {
document.body.classList.remove("loading");
});
Detailed Explanation
<turbo-frame>
is a Hotwire feature that allows you to update only a specific part of a page.
When an action is triggered inside a frame (like clicking a link or submitting a form), Turbo only replaces that frame with the HTML response from the server — without refreshing the full page.
Code Example
// View (index.html.erb)
<turbo-frame id="post_form">
<%= render "form", post: @post %>
</turbo-frame>
// Controller
def edit
@post = Post.find(params[:id])
render partial: "form", locals: { post: @post }
end
// Response (_form.html.erb)
<turbo-frame id="post_form">
<%= form_with model: post do |f| %>
<%= f.text_field :title %>
<%= f.submit %>
<% end %>
</turbo-frame>
Common Questions, Problems & Solutions
❓ Q1: Why doesn’t my turbo-frame update after form submission?
Problem: Submitting a form inside a frame does not change the content.
Solution: Ensure the server response contains a <turbo-frame>
with the exact same id
as the one in the page.
// ✅ In view
<turbo-frame id="edit_post">
<%= render "form" %>
</turbo-frame>
// ✅ In response partial
<turbo-frame id="edit_post">
<p>Post updated!</p>
</turbo-frame>
❓ Q2: How can I redirect to another page after submitting a turbo-frame form?
Problem: Submitting a form inside a frame only replaces the frame. You want to redirect fully.
Solution: Use data-turbo="false"
or add a turbo:submit-end
listener to trigger a JS redirect.
// JS redirect after success
document.addEventListener("turbo:submit-end", (event) => {
if (event.detail.success) {
window.location.href = "/posts";
}
});
❓ Q3: My frame loads, but JavaScript inside it doesn’t work. Why?
Problem: You have JavaScript that should run in the updated frame content, but it doesn’t reinitialize.
Solution: Use the turbo:frame-load
event to re-run any logic after frame content changes.
// JS hook
document.addEventListener("turbo:frame-load", (e) => {
console.log("Frame reloaded:", e.target.id);
// reinit dropdowns, modals, etc.
});
❓ Q4: Can I load content into a turbo-frame without a form?
Problem: You want to update a frame by clicking a link, not a form submission.
Solution: Use a link that targets a controller action returning a frame-wrapped partial.
// View
<turbo-frame id="user_panel">
<a href="/users/1/edit">Edit Profile</a>
</turbo-frame>
// Controller
def edit
@user = User.find(params[:id])
render partial: "form", locals: { user: @user }
end
❓ Q5: Why does my turbo-frame flash before updating?
Problem: The old content disappears briefly before the new one appears.
Solution: Use CSS to add a minimum height or fade animation, or preload with a loader element.
// CSS (optional polish)
turbo-frame {
min-height: 100px;
transition: opacity 0.3s ease;
}
turbo-frame[busy] {
opacity: 0.5;
}
Real-World Scenario
In a blog app, you let users edit a post inline. When they click “Edit,” only the form section (wrapped in a <turbo-frame>
) updates without navigating to a new page.
Alternative Methods or Concepts
- Traditional AJAX with UJS and partial rendering
- Full-page rendering via standard Rails views
- Client-side SPA logic with React/Vue for local state updates
Best Practices with Examples
✅ 1. Always Match Turbo Frame IDs in View and Response
If the server responds with a frame that doesn’t match the one in the DOM, Turbo will ignore it. Make sure the id
is consistent.
// View
<turbo-frame id="profile_form">
<%= render "form" %>
</turbo-frame>
// Partial Response
<turbo-frame id="profile_form">
<p>Profile Updated!</p>
</turbo-frame>
✅ 2. Use Frames for Forms, Tabs, or Inline Editing
Turbo Frames are perfect for updating specific page sections — such as comment forms, profile editors, and modal contents.
// Example usage
<turbo-frame id="comment_form">
<%= render "form" %>
</turbo-frame>
✅ 3. Keep Frame Logic Reusable and Isolated
Treat each frame like a component. Make the partials modular and reusable by avoiding unnecessary dependencies from the outer page.
// Reusable _form.html.erb
<turbo-frame id="comment_form">
<%= form_with model: comment do |f| %>
<%= f.text_area :body %>
<%= f.submit %>
<% end %>
</turbo-frame>
✅ 4. Use turbo:frame-load
to Re-initialize JS
When a frame is updated, your old JavaScript may not run again. Listen to turbo:frame-load
to reinitialize UI behavior.
document.addEventListener("turbo:frame-load", (e) => {
console.log("Frame loaded:", e.target.id);
setupDropdowns(); // or modals, sliders, etc.
});
✅ 5. Optimize UX by Styling Frame States
Use CSS to add a loading state while Turbo updates the frame. This improves perceived performance and user clarity.
// CSS
turbo-frame[busy] {
opacity: 0.6;
pointer-events: none;
}
Implementation
This section walks you through how to implement partial page updates using <turbo-frame>
in a clean, reusable, and user-friendly way.
🔹 Step 1: Wrap dynamic sections in a <turbo-frame>
Decide which part of your page should update (like a form, card, or list item). Wrap that section in a frame with a unique ID.
// app/views/posts/index.html.erb
<turbo-frame id="new_post_form">
<%= render "form", post: Post.new %>
</turbo-frame>
🔹 Step 2: Respond with a partial wrapped in the same turbo-frame ID
In your controller action, render a partial that returns a <turbo-frame>
with the same id
. This ensures Turbo knows what to replace.
// app/controllers/posts_controller.rb
def create
@post = Post.new(post_params)
if @post.save
render partial: "form_success", locals: { post: @post }
else
render partial: "form", status: :unprocessable_entity, locals: { post: @post }
end
end
// app/views/posts/_form_success.html.erb
<turbo-frame id="new_post_form">
<p>Post created successfully!</p>
</turbo-frame>
🔹 Step 3: Use form_with inside the turbo-frame
Turbo will automatically intercept and submit this form via AJAX. No additional JS is needed.
// app/views/posts/_form.html.erb
<turbo-frame id="new_post_form">
<%= form_with model: post do |f| %>
<%= f.text_field :title %>
<%= f.submit "Create Post" %>
<% end %>
</turbo-frame>
🔹 Step 4: Add loading feedback (optional)
You can show users something is happening while the frame is updating. Turbo automatically adds the busy
attribute.
// CSS example
turbo-frame[busy] {
opacity: 0.5;
pointer-events: none;
}
🔹 Step 5: Reinitialize JS behavior after updates
After Turbo replaces a frame, any JS you had will be gone. Use turbo:frame-load
to trigger reinitialization.
// app/javascript/controllers/init.js
document.addEventListener("turbo:frame-load", (e) => {
console.log("Frame loaded:", e.target.id);
initDatePickers(); // or any custom component
});
Detailed Explanation
Turbo Frames allow you to lazy load parts of the page on-demand. When a <turbo-frame src="...">
is used, the content inside that frame is loaded only when needed — reducing initial load time and boosting performance.
Code Example
// View (index.html.erb)
<turbo-frame id="stats_panel" src="/stats">
<div class="loading">Loading stats...</div>
</turbo-frame>
// Controller
def stats
render partial: "stats"
end
// Partial (_stats.html.erb)
<div>
<h3>Live Stats</h3>
<p>Users: <%= User.count %></p>
</div>
Common Questions, Problems & Solutions
❓ Q1: Why isn’t my turbo-frame loading content from the `src`?
Problem: The turbo-frame renders but remains stuck on the placeholder (like “Loading…”).
Solution: Ensure the controller action returns a partial without layout.
// ✅ In controller
def stats
render partial: "stats"
end
// ❌ This will not work inside a turbo-frame:
# render "stats" (this may include layout)
❓ Q2: How do I show a placeholder while the content loads?
Problem: The user sees a blank section while the frame loads.
Solution: Add a minimal message or spinner inside the turbo-frame tag for better UX.
// View
<turbo-frame id="comments" src="/comments">
<p>Loading comments...</p>
</turbo-frame>
❓ Q3: Can I lazy-load content only when it becomes visible?
Problem: You want to load content only when the user scrolls to it.
Solution: Wrap the frame inside a Stimulus controller or use JS with IntersectionObserver
to set the src
dynamically.
// HTML
<turbo-frame id="lazy" data-controller="lazy" data-lazy-src="/profile"></turbo-frame>
// Stimulus (lazy_controller.js)
connect() {
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.element.src = this.element.dataset.lazySrc;
observer.disconnect();
}
});
});
observer.observe(this.element);
}
❓ Q4: My frame loads correctly, but JS inside it doesn’t work.
Problem: Content loads via turbo-frame, but any JS functionality (e.g. dropdown, datepicker) doesn’t work.
Solution: Re-initialize JavaScript inside the loaded frame using the turbo:frame-load
event.
document.addEventListener("turbo:frame-load", (e) => {
if (e.target.id === "comments") {
initDropdowns(); // or tooltips, modals, etc.
}
});
❓ Q5: How can I reload or replace a turbo-frame after it’s rendered?
Problem: You want to refresh or replace the frame’s content without reloading the full page.
Solution: Use a link or button that targets the frame using its id
.
// View
<a href="/stats" data-turbo-frame="stats_panel">Reload Stats</a>
// The target turbo-frame must exist:
<turbo-frame id="stats_panel"></turbo-frame>
Real-World Scenario
In a dashboard, load stats or analytics in a Turbo Frame only when the page is ready — reducing the time it takes for the main content to render and giving a smooth experience to the user.
Alternative Methods or Concepts
- Rails UJS with
remote: true
and manually inserted partials - Client-side lazy loading via IntersectionObserver (JavaScript)
- SPAs with dynamic data loading using AJAX/fetch API
Best Practices with Examples
✅ 1. Use render partial:
in Lazy Frame Controllers
When serving turbo-frame content via src
, always return a partial — not a full layout. Turbo will only replace the frame, and full-page HTML may break the DOM.
// ✅ Correct
def stats
render partial: "stats"
end
// ❌ Avoid this (may include layout)
def stats
render "stats"
end
✅ 2. Include a Placeholder in the Frame
Always add a loading message or spinner inside the frame tag. This gives users visual feedback while content is loading.
// View
<turbo-frame id="profile_panel" src="/profile">
<p>Loading profile...</p>
</turbo-frame>
✅ 3. Keep Frame IDs Unique and Descriptive
Turbo identifies and replaces frames by their ID. Use consistent, semantic IDs to avoid conflicts and make debugging easier.
// ✅ Clear and descriptive
<turbo-frame id="user_card_<%= user.id %>" src="/users/<%= user.id %>/card"></turbo-frame>
✅ 4. Use turbo:frame-load
for JavaScript Reinitialization
When content is lazy-loaded into a frame, existing JS doesn’t automatically apply. Use this event to reinitialize dropdowns, tooltips, or other interactions.
document.addEventListener("turbo:frame-load", (event) => {
if (event.target.id === "profile_panel") {
initTooltips();
}
});
✅ 5. Optimize UX with Visual Frame Styling
Add subtle styles like opacity, transition, or a loader for frames in the loading state. Turbo automatically adds the [busy]
attribute while loading.
// CSS
turbo-frame[busy] {
opacity: 0.5;
pointer-events: none;
transition: opacity 0.3s ease-in-out;
}
Implementation
Lazy loading parts of a page using <turbo-frame>
boosts performance and improves UX by loading only what’s needed, when it’s needed.
Here’s the cleanest and most reliable way to implement this in Rails.
🔹 Step 1: Add a Turbo Frame in your View with a `src`
This frame will automatically make a GET request to the specified src
and replace itself with the response.
Include a loading placeholder for user feedback.
// app/views/dashboard/index.html.erb
<turbo-frame id="stats_panel" src="/dashboard/stats">
<p>Loading stats...</p>
</turbo-frame>
🔹 Step 2: Create a Controller Action That Renders a Partial
The action should return a partial and not a full layout. This is key — Turbo expects only the HTML for the frame content.
// app/controllers/dashboard_controller.rb
def stats
@users = User.active.limit(5)
render partial: "stats_panel", locals: { users: @users }
end
🔹 Step 3: Create a Frame-Wrapped Partial
Use the same frame ID in the partial. This tells Turbo exactly what part of the page to replace.
// app/views/dashboard/_stats_panel.html.erb
<turbo-frame id="stats_panel">
<h3>Top Active Users</h3>
<ul>
<% users.each do |user| %>
<li><%= user.name %></li>
<% end %>
</ul>
</turbo-frame>
🔹 Step 4: Add Styling for a Better Loading Experience
When the frame is loading, Turbo adds the [busy]
attribute. Use this to style your frame during loading.
// app/assets/stylesheets/application.css (or .scss)
turbo-frame[busy] {
opacity: 0.6;
pointer-events: none;
transition: opacity 0.2s ease;
}
🔹 Step 5: Reinitialize JavaScript If Needed After Frame Loads
If the frame includes JavaScript functionality (like dropdowns, date pickers, etc.), reinitialize it using the turbo:frame-load
event.
// app/javascript/controllers/init.js or inside a Stimulus controller
document.addEventListener("turbo:frame-load", (e) => {
if (e.target.id === "stats_panel") {
console.log("Stats panel loaded.");
initTooltips(); // or any JS behavior
}
});
Detailed Explanation
Turbo Frames can be nested to manage complex, component-based layouts. When nesting, only the target frame gets updated — outer frames remain untouched. This allows for dynamic updates of subcomponents without affecting the entire page or layout.
Code Example
// View (app/views/posts/show.html.erb)
<turbo-frame id="post_1">
<h2>Post Title</h2>
<turbo-frame id="comments_post_1" src="/posts/1/comments">
<p>Loading comments...</p>
</turbo-frame>
<turbo-frame id="new_comment_form_1" src="/posts/1/comments/new">
<p>Loading form...</p>
</turbo-frame>
</turbo-frame>
Common Questions, Problems & Solutions
❓ Q1: My nested frame isn’t loading content from its `src`
Problem: You set a src
on a nested frame but it stays stuck on the placeholder or shows nothing.
Solution: Make sure the server response is a partial without layout, and that the route/controller action is returning the correct content.
// ✅ Controller
def comments
@post = Post.find(params[:post_id])
@comments = @post.comments
render partial: "comments", locals: { comments: @comments }
end
// ✅ View
<turbo-frame id="comments_post_1" src="/posts/1/comments">
<p>Loading comments...</p>
</turbo-frame>
❓ Q2: Why is the parent frame reloading when only the child should update?
Problem: You click a button inside a nested frame, but it updates the whole parent frame or page.
Solution: Make sure forms and links inside child frames do not target the outer frame. Use data-turbo-frame="child_frame_id"
or wrap them inside the correct frame tag.
// ✅ Correct nesting and targeting
<turbo-frame id="parent_frame">
<turbo-frame id="child_frame">
<%= form_with url: "/nested", data: { turbo_frame: "child_frame" } do |f| %>
<%= f.submit "Update Child" %>
<% end %>
</turbo-frame>
</turbo-frame>
❓ Q3: The nested frame doesn’t update even though I see the request in DevTools
Problem: A request is made, but Turbo doesn’t replace the content.
Solution: The response must contain a frame with the exact same id
as the one it’s replacing.
// ✅ Response from server
<turbo-frame id="child_frame">
<p>Updated content here!</p>
</turbo-frame>
❓ Q4: My JavaScript stops working inside the nested frame after update
Problem: You’re using dropdowns, modals, or other UI that breaks after the frame is replaced.
Solution: Use the turbo:frame-load
event to reinitialize functionality inside the frame.
// ✅ JavaScript
document.addEventListener("turbo:frame-load", (event) => {
if (event.target.id === "child_frame") {
initDropdowns();
}
});
❓ Q5: Can I nest multiple levels of frames?
Problem: You need 2-3 levels of dynamic UI, like a profile > tabs > editable sections, but updates break or overlap.
Solution: Yes, but test deeply. Ensure each frame has a unique id
and only the target frame gets replaced.
// ✅ Triple nesting
<turbo-frame id="user_1">
<turbo-frame id="user_1_tabs">
<turbo-frame id="user_1_bio_form">
...
</turbo-frame>
</turbo-frame>
</turbo-frame>
Real-World Scenario
A blog post has two interactive areas: a list of comments and a comment form. Each can update independently without affecting the post’s title or content. Nesting Turbo Frames allows isolated updates for both sections.
Alternative Methods or Concepts
- Use Stimulus controllers to toggle sections without replacing HTML
- Use Turbo Streams for live updates across frames
- Render partials with conditional logic instead of nesting
Best Practices with Examples
✅ 1. Use Clear and Unique Frame IDs
Always scope your frame IDs uniquely, especially when nesting. This prevents unexpected behavior where Turbo updates the wrong frame.
// ✅ Unique IDs based on context
<turbo-frame id="post_1">
<turbo-frame id="post_1_comments">
...
</turbo-frame>
</turbo-frame>
✅ 2. Return a Matching Frame in Server Responses
The frame rendered by the server must match the ID of the frame being updated — or Turbo won’t replace it.
// ✅ Frame ID in response must match the one in the view
// app/views/comments/_list.html.erb
<turbo-frame id="post_1_comments">
<%= render @comments %>
</turbo-frame>
✅ 3. Avoid Nested Frame Conflicts by Targeting Correct Frames
Forms or links inside nested frames should use data-turbo-frame
to explicitly specify the target frame, avoiding outer frame replacements.
// ✅ Only child frame is updated
<%= form_with url: comment_path(@comment), data: { turbo_frame: "post_1_comment_form" } do |f| %>
<%= f.text_area :body %>
<%= f.submit %>
<% end %>
✅ 4. Keep Nested Frames Focused and Purposeful
Avoid deeply nested frames unless necessary. Each frame should serve a clear, isolated purpose like loading a form, tab, or section content.
// ✅ Logical separation
<turbo-frame id="profile_section">
<turbo-frame id="edit_profile_form">
<%= render "form" %>
</turbo-frame>
<turbo-frame id="user_activity_logs">
...
</turbo-frame>
</turbo-frame>
✅ 5. Use turbo:frame-load
to Handle JS Reinitialization
Nested frames replace only part of the page, so global JS won’t re-run. Use this event to reattach tooltips, modals, or inputs inside nested content.
// ✅ JS hook for nested frame behavior
document.addEventListener("turbo:frame-load", (event) => {
if (event.target.id.includes("comment_form")) {
initMentions(); // or any other JS
}
});
Implementation
Nested Turbo Frames allow isolated updates for each section of your UI. Below is the best implementation strategy using a blog post page with a comments list and comment form.
🔹 Step 1: Define Your Outer and Inner Turbo Frames
Wrap your entire section (e.g., a blog post) in an outer frame, then nest additional frames for each dynamic part such as comments and forms.
// app/views/posts/show.html.erb
<turbo-frame id="post_<%= @post.id %>">
<h2><%= @post.title %></h2>
<p><%= @post.body %></p>
<turbo-frame id="post_<%= @post.id %>_comments" src="/posts/<%= @post.id %>/comments">
<p>Loading comments...</p>
</turbo-frame>
<turbo-frame id="post_<%= @post.id %>_new_comment" src="/posts/<%= @post.id %>/comments/new">
<p>Loading comment form...</p>
</turbo-frame>
</turbo-frame>
🔹 Step 2: Build Controller Actions for Each Frame
Each frame should be backed by a Rails action that renders a partial (not a full page) using the same id
as the frame.
// app/controllers/comments_controller.rb
def index
@post = Post.find(params[:post_id])
@comments = @post.comments
render partial: "comments/list", locals: { comments: @comments, post: @post }
end
def new
@post = Post.find(params[:post_id])
@comment = @post.comments.build
render partial: "comments/form", locals: { comment: @comment, post: @post }
end
🔹 Step 3: Create Partial Views for Nested Frame Responses
Match the frame id
in the response with the frame in the original page so Turbo knows what to update.
// app/views/comments/_list.html.erb
<turbo-frame id="post_<%= post.id %>_comments">
<h4>Comments</h4>
<ul>
<% comments.each do |comment| %>
<li><%= comment.body %></li>
<% end %>
</ul>
</turbo-frame>
// app/views/comments/_form.html.erb
<turbo-frame id="post_<%= post.id %>_new_comment">
<%= form_with model: [post, comment], data: { turbo_frame: "post_#{post.id}_comments" } do |f| %>
<%= f.text_area :body %>
<%= f.submit "Post Comment" %>
<% end %>
</turbo-frame>
🔹 Step 4: Re-initialize JavaScript Behavior If Needed
When Turbo updates a nested frame, your custom JS (like tooltips, modals, or inputs) might disappear. Use turbo:frame-load
to re-run your JS logic.
// app/javascript/controllers/frame_controller.js
document.addEventListener("turbo:frame-load", (event) => {
if (event.target.id.includes("_comments")) {
highlightNewComment();
}
});
🔹 Step 5: Add Styling for Nested Frames
Use the [busy]
attribute to style loading states within frames, improving the user experience with feedback.
// app/assets/stylesheets/application.css
turbo-frame[busy] {
opacity: 0.6;
pointer-events: none;
transition: opacity 0.2s ease;
}
Detailed Explanation
Broadcasting updates to clients allows real-time UI updates across multiple browser tabs or users without needing manual refreshes. Turbo Streams use ActionCable (WebSockets) to automatically reflect database changes in the UI.
Code Example
// Model (app/models/comment.rb)
class Comment < ApplicationRecord
belongs_to :post
broadcasts_to :post
end
// View (app/views/posts/show.html.erb)
<turbo-stream-from channel=\"PostChannel\" signed-stream-name=\"<%= @post.to_gid_param %>\" />
// Partial (_comment.html.erb)
<turbo-stream action=\"append\" target=\"comments\">
<template>
<%= render comment %>
</template>
</turbo-stream>
Common Questions, Problems & Solutions
❓ Q1: Why are Turbo Stream updates not appearing in real-time?
Problem: A new record is created or updated, but other clients don’t see any change.
Solution: Ensure your model is broadcasting properly using broadcasts_to
or after_create_commit
and that your view is subscribed using <turbo-stream-from>
.
// ✅ Model
class Comment < ApplicationRecord
belongs_to :post
broadcasts_to :post
end
// ✅ View
<turbo-stream-from target=\"<%= dom_id(@post) %>\" />
❓ Q2: The Turbo Stream tag renders as plain HTML instead of working
Problem: Your Turbo Stream broadcast isn’t being interpreted correctly by the browser.
Solution: Make sure you’re broadcasting with the correct MIME type (i.e., text/vnd.turbo-stream.html
) or using Rails’ built-in Turbo helpers.
// ✅ Correct stream response (e.g. in controller)
respond_to do |format|
format.turbo_stream
format.html { redirect_to post_path(@post) }
end
❓ Q3: Turbo Streams don’t update even though a record is saved
Problem: You saved a record, but the stream wasn’t sent.
Solution: Ensure your partial uses the correct DOM target (i.e., ID or class) and matches what’s in the target
attribute.
// ✅ Turbo Stream example
<turbo-stream action=\"append\" target=\"comments\">
<template>
<%= render @comment %>
</template>
</turbo-stream>
// ✅ In your view
<div id=\"comments\"></div>
❓ Q4: How do I scope broadcasts so only related clients receive updates?
Problem: You want only users on a specific page or resource to receive the stream update.
Solution: Use signed_stream_name
with a polymorphic Global ID to limit scope.
// ✅ View
<turbo-stream-from channel=\"PostChannel\" signed-stream-name=\"<%= @post.to_gid_param %>\" />
❓ Q5: My development updates work, but fail in production
Problem: Streams work locally, but nothing happens on your production site.
Solution: Check that ActionCable is enabled in production and is properly mounted in config/cable.yml
and routes.rb
. Also verify Redis or your cable adapter is running.
// ✅ config/routes.rb
mount ActionCable.server => '/cable'
// ✅ config/cable.yml (production)
production:
adapter: redis
url: redis://localhost:6379/1
Real-World Scenario
A shared team dashboard updates task status in real-time across all users’ screens when any team member modifies a task. Turbo Streams handle this instantly using model broadcasting.
Alternative Methods or Concepts
- Pusher or Ably for 3rd-party WebSocket broadcasting
- Polling with
setInterval
and JSON API responses - StimulusReflex or AnyCable for advanced real-time features
Best Practices with Examples
✅ 1. Use broadcasts_to
in Your Models
This Rails helper automatically manages broadcasting when records are created, updated, or destroyed. It’s cleaner than manual broadcasting.
// app/models/comment.rb
class Comment < ApplicationRecord
belongs_to :post
broadcasts_to :post
end
✅ 2. Scope Updates with turbo-stream-from
and GIDs
Use signed stream names to scope Turbo Streams to specific models or users. This prevents unnecessary updates on unrelated pages.
// app/views/posts/show.html.erb
<turbo-stream-from channel=\"PostChannel\" signed-stream-name=\"<%= @post.to_gid_param %>\" />
✅ 3. Use Semantic DOM Targets
Always give the element you want to update a clear, predictable ID so Turbo Streams can append, prepend, or replace properly.
// app/views/posts/show.html.erb
<div id=\"comments\">
<%= render @post.comments %>
</div>
// app/views/comments/create.turbo_stream.erb
<turbo-stream action=\"append\" target=\"comments\">
<template>
<%= render @comment %>
</template>
</turbo-stream>
✅ 4. Use Partial Templates for Broadcasted Content
Turbo Streams render partials inside templates. Keeping these partials clean and reusable avoids bugs and makes updates smoother.
// app/views/comments/_comment.html.erb
<div id=\"comment_<%= comment.id %>\">
<strong><%= comment.user.name %>:</strong>
<p><%= comment.body %></p>
</div>
✅ 5. Gracefully Fallback When WebSocket Is Unavailable
If the client has WebSockets disabled, allow fallback to regular HTML rendering with redirects or conditional logic.
// app/controllers/comments_controller.rb
def create
@comment = @post.comments.create(comment_params)
respond_to do |format|
format.turbo_stream
format.html { redirect_to post_path(@post), notice: \"Comment posted.\" }
end
end
Implementation
Broadcasting updates using Turbo Streams allows your Rails app to update content on all clients in real-time, like new comments appearing on every user’s screen without a page refresh. Here’s a full implementation using Turbo Streams + ActionCable.
🔹 Step 1: Enable broadcasting in your model
Use broadcasts_to
to automatically trigger Turbo Streams when the model is created, updated, or destroyed.
// app/models/comment.rb
class Comment < ApplicationRecord
belongs_to :post
broadcasts_to :post # this is all you need for real-time magic
end
🔹 Step 2: Display the list and set up Turbo Stream subscription
Use turbo-stream-from
in the view to subscribe to updates for the resource (e.g., a post). Use the post’s Global ID as the stream name.
// app/views/posts/show.html.erb
<h2><%= @post.title %></h2>
<div id=\"comments\">
<%= render @post.comments %>
</div>
<turbo-stream-from channel=\"Turbo::StreamsChannel\" signed-stream-name=\"<%= @post.to_gid_param %>\" />
🔹 Step 3: Set up Turbo Stream response template
When a comment is created, Rails will look for a Turbo Stream response. This partial defines how the new comment is added to the page.
// app/views/comments/create.turbo_stream.erb
<turbo-stream action=\"append\" target=\"comments\">
<template>
<%= render @comment %>
</template>
</turbo-stream>
🔹 Step 4: Create a reusable partial for the comment
The partial used inside the stream template should match the structure rendered on the main page.
// app/views/comments/_comment.html.erb
<div id=\"comment_<%= comment.id %>\" class=\"comment\">
<strong><%= comment.user.name %></strong>:
<p><%= comment.body %></p>
</div>
🔹 Step 5: Controller response with fallback
The controller should support both Turbo Stream and HTML responses to handle JavaScript-disabled clients or normal form posts.
// app/controllers/comments_controller.rb
def create
@post = Post.find(params[:post_id])
@comment = @post.comments.new(comment_params)
@comment.user = current_user
if @comment.save
respond_to do |format|
format.turbo_stream
format.html { redirect_to post_path(@post), notice: \"Comment added.\" }
end
else
render :new, status: :unprocessable_entity
end
end
🔹 Step 6: Mount ActionCable in routes and config
Ensure ActionCable is mounted and running in development and production environments.
// config/routes.rb
mount ActionCable.server => \"/cable\"
// config/cable.yml (development)
development:
adapter: async
// config/cable.yml (production)
production:
adapter: redis
url: redis://localhost:6379/1
Detailed Explanation
Using Turbo Streams, Rails allows you to reflect changes (create, update, delete) instantly across all connected clients without manual reloads. These operations are triggered via WebSocket (ActionCable) and rendered using Turbo Stream templates.
Code Example
// Model
class Task < ApplicationRecord
broadcasts_to ->(task) { :tasks }
end
// Create Turbo Stream (create.turbo_stream.erb)
<turbo-stream action=\"append\" target=\"tasks\">
<template>
<%= render @task %>
</template>
</turbo-stream>
// Update Turbo Stream (update.turbo_stream.erb)
<turbo-stream action=\"replace\" target=\"task_<%= @task.id %>\">
<template>
<%= render @task %>
</template>
</turbo-stream>
// Destroy Turbo Stream (destroy.turbo_stream.erb)
<turbo-stream action=\"remove\" target=\"task_<%= @task.id %>\" />
Common Questions, Problems & Solutions
❓ Q1: Why doesn’t the new record appear in the list automatically?
Problem: You create a new record, but nothing updates on other users’ screens.
Solution: Add broadcasts_to
to the model and use Turbo Stream from the correct target in the view.
// Model
class Task < ApplicationRecord
broadcasts_to ->(task) { :tasks }
end
// View
<turbo-stream-from target=\"tasks\" />
❓ Q2: My update renders, but nothing changes in the UI
Problem: Turbo Stream doesn’t know which element to replace.
Solution: Use a unique DOM ID (like id=\"task_1\"
) and match it in the stream template with target=\"task_1\"
.
// View
<div id=\"task_<%= task.id %>\">...</div>
// update.turbo_stream.erb
<turbo-stream action=\"replace\" target=\"task_<%= @task.id %>\">
<template><%= render @task %></template>
</turbo-stream>
❓ Q3: Deleted items don’t disappear on other clients
Problem: Delete works locally but not for other connected users.
Solution: Add a destroy.turbo_stream.erb
that broadcasts the removal action with the correct target.
// destroy.turbo_stream.erb
<turbo-stream action=\"remove\" target=\"task_<%= @task.id %>\" />
❓ Q4: Turbo Stream works locally but not across tabs or users
Problem: WebSocket connection is not active or ActionCable is not configured in production.
Solution: Ensure ActionCable
is mounted in routes, configured properly, and Redis is running in production.
// config/routes.rb
mount ActionCable.server => '/cable'
// config/cable.yml
production:
adapter: redis
url: redis://localhost:6379/1
❓ Q5: How do I handle fallback for Turbo Stream if JS is disabled?
Problem: Some clients don’t have JavaScript or WebSocket enabled.
Solution: Add HTML redirect fallback in your controller action using respond_to
.
// Controller
respond_to do |format|
format.turbo_stream
format.html { redirect_to tasks_path, notice: \"Updated.\" }
end
Real-World Scenario
In a team Kanban board, when a user creates, edits, or deletes a task, all team members see updates live without refreshing the page — making collaboration seamless.
Alternative Methods or Concepts
- Polling with AJAX every few seconds
- Using external services like Pusher or Ably
- StimulusReflex for more granular updates
Best Practices with Examples
✅ 1. Use Consistent and Unique DOM IDs for Each Resource
Always wrap individual resources (e.g., tasks, posts) with a predictable DOM ID so Turbo Streams can find and manipulate them easily.
// app/views/tasks/_task.html.erb
<div id=\"task_<%= task.id %>\" class=\"task-item\">
<p><%= task.title %></p>
</div>
✅ 2. Separate Turbo Stream Templates for Each Operation
Create separate `.turbo_stream.erb` files for create
, update
, and destroy
to handle real-time responses cleanly.
// create.turbo_stream.erb
<turbo-stream action=\"append\" target=\"tasks\">
<template>
<%= render @task %>
</template>
</turbo-stream>
// update.turbo_stream.erb
<turbo-stream action=\"replace\" target=\"task_<%= @task.id %>\">
<template>
<%= render @task %>
</template>
</turbo-stream>
// destroy.turbo_stream.erb
<turbo-stream action=\"remove\" target=\"task_<%= @task.id %>\" />
✅ 3. Broadcast from Models Using broadcasts_to
Rails automatically handles broadcasting when you use broadcasts_to
, keeping your controller clean and logic centralized.
// app/models/task.rb
class Task < ApplicationRecord
broadcasts_to ->(task) { :tasks }
end
✅ 4. Subscribe to Updates with turbo-stream-from
Use this tag to subscribe to Turbo Stream updates in your view. It listens for changes and automatically renders them on the client.
// app/views/tasks/index.html.erb
<turbo-stream-from target=\"tasks\" />
<div id=\"tasks\">
<%= render @tasks %>
</div>
✅ 5. Provide Fallback Responses for Non-JavaScript Clients
Not all users may have JavaScript or WebSockets enabled. Always support a graceful fallback using standard HTML redirects.
// app/controllers/tasks_controller.rb
def create
@task = Task.new(task_params)
if @task.save
respond_to do |format|
format.turbo_stream
format.html { redirect_to tasks_path, notice: \"Task added.\" }
end
else
render :new, status: :unprocessable_entity
end
end
Implementation
Real-time create, update, and delete (CRUD) operations in Rails are best achieved using Turbo Streams combined with model broadcasting. Here’s a full implementation example using a Task
model with partials and Turbo responses.
🔹 Step 1: Enable Broadcasting in the Model
Use broadcasts_to
so Turbo can automatically broadcast changes via ActionCable.
// app/models/task.rb
class Task < ApplicationRecord
broadcasts_to ->(task) { :tasks }
end
🔹 Step 2: Render Turbo Stream Subscription and Task List
Use turbo-stream-from
in your view to listen to the stream.
// app/views/tasks/index.html.erb
<turbo-stream-from target="tasks" />
<div id="tasks">
<%= render @tasks %>
</div>
🔹 Step 3: Create Turbo Stream Templates
Use one for each action: create, update, and destroy.
// app/views/tasks/create.turbo_stream.erb
<turbo-stream action="append" target="tasks">
<template><%= render @task %></template>
</turbo-stream>
// app/views/tasks/update.turbo_stream.erb
<turbo-stream action="replace" target="task_<%= @task.id %>">
<template><%= render @task %></template>
</turbo-stream>
// app/views/tasks/destroy.turbo_stream.erb
<turbo-stream action="remove" target="task_<%= @task.id %>" />
🔹 Step 4: Use Consistent DOM IDs
Each task should be rendered with a unique DOM ID matching the Turbo Stream target.
// app/views/tasks/_task.html.erb
<div id="task_<%= task.id %>">
<p><%= task.title %></p>
<%= link_to 'Edit', edit_task_path(task) %>
<%= link_to 'Delete', task_path(task), method: :delete, data: { turbo_confirm: "Are you sure?" } %>
</div>
🔹 Step 5: Handle Turbo and HTML in the Controller
Add respond_to
for both turbo stream and HTML responses.
// app/controllers/tasks_controller.rb
def create
@task = Task.new(task_params)
if @task.save
respond_to do |format|
format.turbo_stream
format.html { redirect_to tasks_path, notice: "Task created." }
end
else
render :new, status: :unprocessable_entity
end
end
def update
if @task.update(task_params)
respond_to do |format|
format.turbo_stream
format.html { redirect_to tasks_path, notice: "Task updated." }
end
else
render :edit, status: :unprocessable_entity
end
end
def destroy
@task.destroy
respond_to do |format|
format.turbo_stream
format.html { redirect_to tasks_path, notice: "Task deleted." }
end
end
🔹 Step 6: Ensure ActionCable is Running
Don’t forget to mount ActionCable and set up Redis if in production.
// config/routes.rb
mount ActionCable.server => '/cable'
// config/cable.yml
production:
adapter: redis
url: redis://localhost:6379/1
Detailed Explanation
ActionCable integrates WebSockets into Rails. It allows real-time features like chat, notifications, live updates, and broadcasting data from server to client instantly using channels and subscriptions.
Code Example
// app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from \"chat_#{params[:room]}\"
end
end
// app/javascript/channels/chat_channel.js
import consumer from \"./consumer\"
consumer.subscriptions.create({ channel: \"ChatChannel\", room: \"general\" }, {
received(data) {
const chatBox = document.getElementById(\"messages\")
chatBox.insertAdjacentHTML(\"beforeend\", data.html)
}
})
Common Questions, Problems & Solutions
❓ Q1: Why isn’t my WebSocket connecting?
Problem: You see no connection or errors like “WebSocket connection failed” in the browser console.
Solution: Ensure ActionCable is mounted in routes.rb
, allowed in your environment config, and Redis is running (in production).
// config/routes.rb
mount ActionCable.server => '/cable'
// config/environments/production.rb
config.action_cable.allowed_request_origins = [ 'https://yourdomain.com' ]
❓ Q2: My JavaScript receives nothing even though the broadcast happens
Problem: You broadcast data but the client doesn’t update.
Solution: Check your stream name and ensure the client is subscribed to the same identifier.
// Ruby Channel
stream_from \"chat_#{params[:room]}\"
// JS
consumer.subscriptions.create({ channel: \"ChatChannel\", room: \"general\" }, {
received(data) {
console.log(data); // check if data received
}
})
❓ Q3: Messages broadcast twice or to the wrong room
Problem: Streams aren’t scoped properly so users see others’ messages.
Solution: Use `stream_for` with a specific resource or user.
// Better: app/channels/chat_channel.rb
stream_for current_user
// Broadcast:
ChatChannel.broadcast_to(current_user, { html: \"...\" })
❓ Q4: WebSocket disconnects frequently or fails silently
Problem: The connection dies due to inactivity or server limits.
Solution: Configure your proxy/server (e.g., NGINX) to support persistent WebSocket connections.
// nginx.conf example
location /cable {
proxy_pass http://your-app;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
}
❓ Q5: How can I test ActionCable functionality locally?
Problem: You’re unsure how to validate ActionCable works in development.
Solution: Open the app in two browser windows, subscribe to a channel, and broadcast a message from Rails console.
// Rails console
ActionCable.server.broadcast(\"chat_general\", { html: \"<p>Live test</p>\" })
Real-World Scenario
A live chat feature on a blog lets users communicate in real-time. Messages are sent and received via ActionCable using channels for each room.
Alternative Methods or Concepts
- Pusher or Ably for real-time push services
- Polling with `setInterval()` and Ajax
- StimulusReflex for full DOM reflexes
Best Practices with Examples
✅ 1. Use stream_for
for Scoped Broadcasting
Prefer stream_for
to stream_from
when targeting users or resources. It ensures broadcasts are limited to a specific context.
// app/channels/chat_channel.rb
def subscribed
stream_for current_user
end
// Anywhere in Rails
ChatChannel.broadcast_to(current_user, { html: render_message })
✅ 2. Render Partials or Turbo Streams for Broadcast Data
Always use view partials or Turbo Streams to maintain consistency in frontend rendering.
// app/channels/message_broadcast.rb
html = ApplicationController.renderer.render(
partial: 'messages/message', locals: { message: @message }
)
ActionCable.server.broadcast(\"chat_general\", { html: html })
✅ 3. Disconnect Cleanly and Avoid Duplicate Subscriptions
Always implement the unsubscribed
method and clean up connections to avoid memory leaks or duplicate messages.
// chat_channel.rb
def unsubscribed
Rails.logger.info \"User #{current_user.id} disconnected.\"
end
✅ 4. Use Redis in Production
ActionCable defaults to in-memory (async) in development. For production, always use Redis to scale and persist messages.
// config/cable.yml
production:
adapter: redis
url: redis://localhost:6379/1
✅ 5. Secure ActionCable with Origin and Authorization
Prevent unauthorized WebSocket connections by validating request origins and authenticating users.
// config/environments/production.rb
config.action_cable.allowed_request_origins = [ 'https://yourapp.com' ]
// app/channels/application_cable/connection.rb
identified_by :current_user
def connect
self.current_user = find_verified_user
end
Implementation
Here’s a complete, real-world implementation of ActionCable in Rails for a live chat system. We’ll walk through generating a channel, broadcasting messages, rendering them in real time, and securing connections.
🔹 Step 1: Generate a Channel
Create a chat channel to handle subscriptions.
$ rails generate channel Chat
This generates chat_channel.rb
(server) and chat_channel.js
(client).
🔹 Step 2: Define Stream Logic on the Server
Set up your subscription stream in the generated channel.
// app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from "chat_#{params[:room]}"
end
def unsubscribed
Rails.logger.info "User disconnected from chat_#{params[:room]}"
end
end
🔹 Step 3: Create the Client-side Subscription
Connect to the stream and handle incoming data.
// app/javascript/channels/chat_channel.js
import consumer from "./consumer"
consumer.subscriptions.create({ channel: "ChatChannel", room: "general" }, {
connected() {
console.log("Connected to general room")
},
disconnected() {
console.log("Disconnected")
},
received(data) {
const container = document.getElementById("messages")
container.insertAdjacentHTML("beforeend", data.html)
}
})
🔹 Step 4: Broadcast a Message
From your controller or model, broadcast messages to the channel.
// app/controllers/messages_controller.rb
class MessagesController < ApplicationController
def create
@message = Message.create!(message_params)
html = ApplicationController.renderer.render(partial: 'messages/message', locals: { message: @message })
ActionCable.server.broadcast("chat_general", { html: html })
head :ok
end
private
def message_params
params.require(:message).permit(:body)
end
end
🔹 Step 5: Render Each Message as a Partial
Keep UI rendering clean by isolating it to a partial.
// app/views/messages/_message.html.erb
<div class="message">
<p><%= message.body %></p>
</div>
🔹 Step 6: Enable Redis and Mount ActionCable
For production, ensure Redis is running and ActionCable is mounted.
// config/routes.rb
mount ActionCable.server => '/cable'
// config/cable.yml
production:
adapter: redis
url: redis://localhost:6379/1
🔹 Step 7: Secure the Connection
Authenticate users when they connect to ActionCable.
// app/channels/application_cable/connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
end
private
def find_verified_user
User.find_by(id: cookies.encrypted[:user_id]) || reject_unauthorized_connection
end
end
end
Detailed Explanation
Turbo + Stimulus enables interactive, real-time forms that provide immediate feedback or actions without full-page reloads. Turbo handles HTML partial rendering while Stimulus listens for events like input or form submit to enhance UX.
Implementation
Here’s a full implementation of a live form using Turbo Frames + Stimulus in a Rails app. The form will allow real-time preview of the title input as the user types and handle creation with inline errors.
🔹 Step 1: Generate a Resource
Create a simple Post resource.
$ rails generate scaffold Post title:string body:text
🔹 Step 2: Create a Turbo Frame for the Form
Wrap your form in a <turbo-frame>
and make sure the frame ID matches the data-turbo-frame
in your form.
// app/views/posts/new.html.erb
<turbo-frame id="post_form">
<%= render "form", post: @post %>
</turbo-frame>
🔹 Step 3: Build the Form Partial with Stimulus
Create a form that shows a live preview of the title field using Stimulus.
// app/views/posts/_form.html.erb
<div data-controller="live-form">
<%= form_with(model: post, data: { turbo_frame: "post_form" }) do |f| %>
<div>
<%= f.label :title %>
<%= f.text_field :title, data: {
live_form_target: "input",
action: "input->live-form#updatePreview"
} %>
</div>
<p>
Preview: <span data-live-form-target="preview"></span>
</p>
<div>
<%= f.label :body %>
<%= f.text_area :body %>
</div>
<div>
<%= f.submit %>
</div>
<% end %>
</div>
🔹 Step 4: Create the Stimulus Controller
This controller updates the preview span when the input field changes.
// app/javascript/controllers/live_form_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["input", "preview"]
updatePreview() {
this.previewTarget.textContent = this.inputTarget.value
}
}
🔹 Step 5: Render Turbo Stream on Create
If the form submission fails, re-render the form inside the frame to show errors.
// app/views/posts/create.turbo_stream.erb
<turbo-frame id="post_form">
<%= render "form", post: @post %>
</turbo-frame>
🔹 Step 6: Controller Setup
Respond with Turbo Stream and HTML fallback for JS-disabled clients.
// app/controllers/posts_controller.rb
def create
@post = Post.new(post_params)
if @post.save
respond_to do |format|
format.turbo_stream { render turbo_stream: turbo_stream.append("posts", partial: "posts/post", locals: { post: @post }) }
format.html { redirect_to posts_path, notice: "Post created." }
end
else
respond_to do |format|
format.turbo_stream { render :create }
format.html { render :new, status: :unprocessable_entity }
end
end
end
🔹 Step 7: Optional – Clear Form on Success
You can add a Stimulus method to clear inputs or show a success message.
// Optional addition in Stimulus
resetForm() {
this.inputTarget.value = ""
this.previewTarget.textContent = ""
}
Common Questions, Problems & Solutions
❓ Q1: Why is my Stimulus controller not working?
Problem: Nothing happens when I type in the form or click a button.
Solution: Ensure the controller is correctly named and registered in your JavaScript pack.
// app/javascript/controllers/index.js
import { Application } from \"@hotwired/stimulus\"
import LiveFormController from \"./live_form_controller\"
const application = Application.start()
application.register(\"live-form\", LiveFormController)
❓ Q2: Turbo Frame doesn’t isolate form submission
Problem: Submitting the form reloads the entire page.
Solution: Wrap your form with a Turbo Frame and use data-turbo-frame
properly.
// HTML
<turbo-frame id=\"post_form\">
<%= form_with model: @post, data: { turbo_frame: \"post_form\" } do |f| %>
...
<% end %>
</turbo-frame>
❓ Q3: Live preview doesn’t show updated input
Problem: The preview target doesn’t reflect changes from input.
Solution: Make sure you’re referencing the correct `data-target` in both HTML and JS.
// Stimulus
static targets = [\"input\", \"preview\"]
// HTML
<input data-live-form-target=\"input\" ... />
<span data-live-form-target=\"preview\"></span>
❓ Q4: Why does my form submit twice?
Problem: Submitting via button or keyboard triggers double request.
Solution: Use event.preventDefault()
inside Stimulus if you’re handling form submission manually, or rely on Turbo’s default behavior without overriding it.
// Only handle form if necessary, otherwise let Turbo handle it automatically
submit(event) {
event.preventDefault()
// custom logic (optional)
}
❓ Q5: I’m not seeing errors in the form after submit
Problem: The page does not update or show validation messages.
Solution: Make sure you’re rendering form errors via Turbo response (`.turbo_stream.erb`) or inside the same Turbo Frame.
// app/views/posts/create.turbo_stream.erb
<turbo-frame id=\"post_form\">
<%= render \"form\", post: @post %>
</turbo-frame>
Alternative Methods or Concepts
- Vue.js or React with Rails API
- Unpoly.js for progressive enhancement
- Pure Turbo + partial reloads (no JS)
Best Practices with Examples
✅ 1. Keep Turbo Frame IDs consistent and scoped
Always wrap your form in a `id
matches the form’s data-turbo-frame
attribute. This ensures scoped rendering without full-page reloads.
// In the view
<turbo-frame id=\"post_form\">
<%= form_with model: @post, data: { turbo_frame: \"post_form\" } do |f| %>
<%= f.text_field :title %>
<%= f.submit %>
<% end %>
</turbo-frame>
✅ 2. Use Stimulus for small enhancements only
Don’t overuse Stimulus for everything — just enhance where necessary (like live previews, conditional logic, toggles).
// Stimulus example for preview
static targets = [\"input\", \"preview\"]
updatePreview() {
this.previewTarget.textContent = this.inputTarget.value
}
✅ 3. Show server-side validation errors in Turbo Frames
Return Turbo Stream templates or re-render the form inside the Turbo Frame to display validation feedback.
// app/views/posts/create.turbo_stream.erb
<turbo-frame id=\"post_form\">
<%= render \"form\", post: @post %>
</turbo-frame>
✅ 4. Use data-action
with precision
Use Stimulus `data-action` on specific elements to trigger precise actions, reducing unexpected behavior.
// HTML
<input data-action=\"input->live-form#updatePreview\" />
✅ 5. Handle empty state and user feedback
Always give users live visual feedback — like loading spinners, disabled buttons, or cleared previews after submit.
// Example
this.submitButtonTarget.disabled = true
this.previewTarget.textContent = \"Sending...\"
Real-World Scenario
A blog post editor auto-previews the post title and slug in real-time as the user types. Stimulus updates the DOM immediately, and Turbo handles submission without leaving the page.
Detailed Explanation
Inline editing lets users click on a piece of text (e.g. title, comment) and instantly turn it into a form. Turbo Frames handle seamless form replacement, while Stimulus toggles views and behaviors.
Implementation
Here’s a complete and detailed implementation of inline editing using Turbo Frames and Stimulus in a Rails app. This approach updates individual records in place without navigating away or reloading the page.
🔹 Step 1: Add Turbo Frame and Inline Edit Toggle
Each task row includes both display and edit form, wrapped in a Turbo Frame and managed by Stimulus.
// app/views/tasks/_task.html.erb
<turbo-frame id="task_<%= task.id %>">
<div data-controller="inline-edit">
<div data-inline-edit-target="display">
<p><%= task.title %></p>
<button data-action="click->inline-edit#edit">Edit</button>
</div>
<div data-inline-edit-target="form" hidden>
<%= form_with(model: task, data: { turbo_frame: dom_id(task) }) do |f| %>
<%= f.text_field :title %>
<%= f.submit "Save" %>
<button type="button" data-action="click->inline-edit#cancel">Cancel</button>
<% end %>
</div>
</div>
</turbo-frame>
🔹 Step 2: Create the Stimulus Controller
This controller toggles between view and edit modes.
// app/javascript/controllers/inline_edit_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["display", "form"]
edit() {
this.displayTarget.hidden = true
this.formTarget.hidden = false
}
cancel() {
this.formTarget.hidden = true
this.displayTarget.hidden = false
}
}
🔹 Step 3: Update Controller to Respond with Turbo Stream
Return the updated partial from the controller so Turbo replaces the content inline.
// app/controllers/tasks_controller.rb
def update
@task = Task.find(params[:id])
if @task.update(task_params)
respond_to do |format|
format.turbo_stream { render partial: "tasks/task", locals: { task: @task } }
format.html { redirect_to tasks_path, notice: "Updated successfully." }
end
else
render :edit, status: :unprocessable_entity
end
end
🔹 Step 4: Use Shared Partial for Consistency
Always use _task.html.erb
to render both normal and Turbo responses.
// app/views/tasks/index.html.erb
<%= turbo_frame_tag "tasks" do %>
<%= render @tasks %>
<% end %>
🔹 Step 5: Enable JavaScript and Stimulus Controllers
Ensure Stimulus is installed and controllers are registered.
// app/javascript/controllers/index.js
import { Application } from "@hotwired/stimulus"
import InlineEditController from "./inline_edit_controller"
const application = Application.start()
application.register("inline-edit", InlineEditController)
Common Questions, Problems & Solutions
❓ Q1: Why does the form not appear when I click “Edit”?
Problem: Stimulus controller is not activating the form display logic.
Solution: Ensure your targets are correctly named and the controller is connected in the DOM.
// Stimulus Controller
static targets = [\"display\", \"form\"];
// HTML
<div data-controller=\"inline-edit\">
<div data-inline-edit-target=\"display\">...</div>
<div data-inline-edit-target=\"form\" hidden>...</div>
</div>
❓ Q2: Turbo Frame doesn’t update after submitting the inline form
Problem: The Turbo Frame remains in the edit state after submitting the form.
Solution: Ensure your controller action renders the Turbo Frame’s partial instead of redirecting.
// tasks_controller.rb
def update
if @task.update(task_params)
respond_to do |format|
format.turbo_stream { render partial: \"tasks/task\", locals: { task: @task } }
format.html { redirect_to tasks_path }
end
else
render :edit, status: :unprocessable_entity
end
end
❓ Q3: Editing one task updates all task rows
Problem: Turbo stream or form submission targets multiple elements accidentally.
Solution: Use unique turbo-frame
IDs (e.g., task_1
, task_2
) per row.
// HTML
<turbo-frame id=\"task_<%= task.id %>\">...</turbo-frame>
❓ Q4: Why does the form reload the full page instead of updating inline?
Problem: You’re not submitting the form inside a Turbo Frame.
Solution: Set the form’s data-turbo-frame
attribute to match the frame’s ID.
<%= form_with model: task, data: { turbo_frame: \"task_#{task.id}\" } do |f| %>
...fields...
<% end %>
❓ Q5: How do I cancel editing without reloading?
Problem: You want to allow users to cancel inline editing and return to view mode instantly.
Solution: Add a cancel button in Stimulus to toggle visibility back to display mode.
// Stimulus Method
cancel() {
this.formTarget.hidden = true
this.displayTarget.hidden = false
}
// HTML
<button data-action=\"click->inline-edit#cancel\">Cancel</button>
Alternative Methods or Concepts
- Use Hotwire’s `data-turbo-action=”replace”` for simple swaps
- Use Turbo Streams for server-triggered updates
- Use full-page modal editing for mobile-first interfaces
Best Practices with Examples
✅ 1. Use unique Turbo Frame IDs for each record
Assign a distinct ID like task_1
or user_5
to ensure Turbo swaps the correct DOM element when the form is submitted.
// In view
<turbo-frame id=\"task_<%= task.id %>\">
<%= render task %>
</turbo-frame>
// In form
<%= form_with model: task, data: { turbo_frame: \"task_#{task.id}\" } do |f| %>
...
<% end %>
✅ 2. Wrap both display and form in the same Turbo Frame
This allows Turbo to seamlessly swap display for form and vice versa with no page reload.
<turbo-frame id=\"task_<%= task.id %>\">
<div data-controller=\"inline-edit\">
<div data-inline-edit-target=\"display\">...</div>
<div data-inline-edit-target=\"form\" hidden>...</div>
</div>
</turbo-frame>
✅ 3. Use Stimulus for toggling visibility only
Let Turbo handle the actual swapping and server rendering. Use Stimulus just to toggle the form/display on the client.
// Stimulus
edit() {
this.displayTarget.hidden = true
this.formTarget.hidden = false
}
cancel() {
this.formTarget.hidden = true
this.displayTarget.hidden = false
}
✅ 4. Keep form rendering logic in a shared partial
Reuse a single `_form.html.erb` partial for both inline and full-page edits to avoid duplication.
// app/views/tasks/_form.html.erb
<%= form_with(model: task, data: { turbo_frame: dom_id(task) }) do |f| %>
<%= f.text_field :title %>
<%= f.submit %>
<% end %>
✅ 5. Handle fallback HTML responses for non-JS clients
Always respond with HTML in the controller for users who may have JavaScript disabled.
// tasks_controller.rb
def update
if @task.update(task_params)
respond_to do |format|
format.turbo_stream
format.html { redirect_to tasks_path, notice: \"Updated.\" }
end
else
render :edit
end
end
Real-World Scenario
In a task management dashboard, users can click on a task title and edit it inline without navigating away. When they hit save, the task updates instantly using Turbo Frames, and Stimulus toggles the form visibility.
Detailed Explanation
Using Turbo Frames and Turbo Streams with Rails allows you to validate forms in real-time without reloading. Server-side validation errors can be rendered directly back into the form frame with minimal effort.
Implementation
This is a complete example of handling real-time form errors, validations, and feedback using Turbo Frames and Turbo Streams in a Rails application. It includes validation logic, scoped rendering, inline errors, and top-level alert feedback.
🔹 Step 1: Create the Model with Validations
// app/models/user.rb
class User < ApplicationRecord
validates :email, presence: true, uniqueness: true
validates :username, presence: true
end
🔹 Step 2: Build the Form Inside a Turbo Frame
// app/views/users/new.html.erb
<turbo-frame id="user_form">
<%= render "form", user: @user %>
</turbo-frame>
🔹 Step 3: Create the Reusable Form Partial
// app/views/users/_form.html.erb
<%= form_with model: user, data: { turbo_frame: "user_form" } do |f| %>
<div class="form-group <%= 'field-error' if user.errors[:email].any? %>">
<%= f.label :email %>
<%= f.email_field :email %>
<% if user.errors[:email].any? %>
<div class="error-msg"><%= user.errors[:email].first %></div>
<% end %>
</div>
<div class="form-group <%= 'field-error' if user.errors[:username].any? %>">
<%= f.label :username %>
<%= f.text_field :username %>
<% if user.errors[:username].any? %>
<div class="error-msg"><%= user.errors[:username].first %></div>
<% end %>
</div>
<%= f.submit "Create User" %>
<% end %>
🔹 Step 4: Controller with Turbo Stream + HTML Fallback
// app/controllers/users_controller.rb
class UsersController < ApplicationController
def new
@user = User.new
end
def create
@user = User.new(user_params)
if @user.save
respond_to do |format|
format.turbo_stream { render turbo_stream: turbo_stream.replace("user_form", partial: "users/success") }
format.html { redirect_to root_path, notice: "User created!" }
end
else
respond_to do |format|
format.turbo_stream { render turbo_stream: turbo_stream.replace("user_form", partial: "users/form", locals: { user: @user }) }
format.html { render :new, status: :unprocessable_entity }
end
end
end
private
def user_params
params.require(:user).permit(:email, :username)
end
end
🔹 Step 5: Add a Global Alert Frame for Top Feedback
// app/views/layouts/application.html.erb (or anywhere in the view)
<turbo-frame id="alerts"></turbo-frame>
// app/views/users/create.turbo_stream.erb
<turbo-stream target="alerts" action="update">
<template><div class="alert alert-danger">Please correct the errors below.</div></template>
</turbo-stream>
🔹 Step 6: Style the Errors for Better UX
/* app/assets/stylesheets/application.css */
.field-error input,
.field-error textarea {
border-color: red;
background-color: #fff0f0;
}
.error-msg {
color: red;
font-size: 0.875rem;
margin-top: 4px;
}
Common Questions, Problems & Solutions
❓ Q1: Why are my validation errors not showing up?
Problem: The form submits, but errors are not visible in the UI.
Solution: Make sure the form re-renders inside a Turbo Frame with the errors visible.
// app/views/users/_form.html.erb
<% if user.errors.any? %>
<ul class=\"error-list\">
<% user.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
<% end %>
❓ Q2: Why does the form redirect instead of staying on the same page?
Problem: Turbo is not catching the error response and falling back to a redirect.
Solution: Respond with format.turbo_stream
inside your controller.
// app/controllers/users_controller.rb
def create
@user = User.new(user_params)
if @user.save
redirect_to root_path, notice: \"Created\"
else
respond_to do |format|
format.turbo_stream { render partial: \"form\", locals: { user: @user } }
format.html { render :new, status: :unprocessable_entity }
end
end
end
❓ Q3: Why is my form submitting but nothing changes?
Problem: You forgot to wrap the form in a Turbo Frame or the Frame ID mismatches.
Solution: Use the same ID for the Turbo Frame and form response target.
// in new.html.erb
<turbo-frame id=\"user_form\">
<%= render \"form\", user: @user %>
</turbo-frame>
<%= form_with model: @user, data: { turbo_frame: \"user_form\" } do |f| %>
❓ Q4: How can I highlight invalid fields?
Problem: Users can’t tell which field is invalid.
Solution: Add error styling to fields based on error presence.
// form partial
<div class=\"form-group <%= 'field-error' if user.errors[:email].any? %>\">
<%= f.label :email %>
<%= f.email_field :email %>
<span class=\"error-msg\"><%= user.errors[:email].first %></span>
</div>
/* CSS */
.field-error input {
border-color: red;
}
.error-msg {
color: red;
font-size: 0.875rem;
}
❓ Q5: How do I show a global message on failure?
Problem: You want a top-level alert for form errors.
Solution: Use a Turbo Frame to update an alert banner.
// layout or view
<turbo-frame id=\"alerts\"></turbo-frame>
// in create.turbo_stream.erb
<turbo-stream target=\"alerts\" action=\"update\">
<template><div class=\"alert alert-danger\">Please fix the errors below.</div></template>
</turbo-stream>
Alternative Methods or Concepts
- Client-side validations using StimulusReflex or custom JS
- Using Flash messages inside Turbo Frames
- AJAX + jQuery + Rails UJS (legacy setup)
Best Practices with Examples
✅ 1. Keep Forms Inside Scoped Turbo Frames
Always wrap your forms in a <turbo-frame>
and match the frame ID with the form’s data-turbo-frame
. This ensures the form errors render inside the same frame instead of causing a full reload.
// app/views/users/new.html.erb
<turbo-frame id=\"user_form\">
<%= render \"form\", user: @user %>
</turbo-frame>
// app/views/users/_form.html.erb
<%= form_with model: user, data: { turbo_frame: \"user_form\" } do |f| %>
<%= f.text_field :email %>
<% end %>
✅ 2. Show Inline Validation Errors Clearly
Use Rails’ built-in error handling methods to show helpful messages near fields.
// Inside form
<%= f.text_field :email, class: (user.errors[:email].any? ? 'input-error' : '') %>
<% if user.errors[:email].any? %>
<div class=\"error-msg\"><%= user.errors[:email].first %></div>
<% end %>
/* CSS */
.input-error {
border: 1px solid red;
}
.error-msg {
color: red;
font-size: 0.875rem;
}
✅ 3. Use Turbo Streams for Global Feedback
Display success or failure messages in a top banner using a dedicated Turbo Frame.
// layout
<turbo-frame id=\"alerts\"></turbo-frame>
// turbo_stream response
<turbo-stream target=\"alerts\" action=\"update\">
<template><div class=\"alert alert-danger\">Form failed to submit.</div></template>
</turbo-stream>
✅ 4. Don’t Depend on JS for Critical Validation
Always perform server-side validation in your model. Client-side validation should only be a UX enhancement.
// app/models/user.rb
validates :email, presence: true, uniqueness: true
✅ 5. Use Stimulus Only for Enhancing UX (e.g. live previews)
Let Turbo and server handle validation feedback. Use Stimulus to show/hide content or preview input values.
// app/javascript/controllers/form_controller.js
updatePreview() {
this.previewTarget.textContent = this.inputTarget.value
}
Real-World Scenario
In a sign-up form, the user enters an invalid email. Instead of redirecting, Turbo Streams instantly show the validation error in the form without refreshing the page — keeping the user on the same screen with instant feedback.
Detailed Explanation
Turbo Streams work great with server-rendered content. By combining them with Rails fragment caching, you can update content efficiently without hitting the database or rendering unnecessary views again.
When a broadcast updates a Turbo Stream, if the rendered content is cached, the update is much faster and scalable.
Implementation
This is a complete implementation showing how to use Rails fragment caching in combination with Turbo Frames and Turbo Streams for efficient, real-time updates.
🔹 Step 1: Enable Caching in Development (if not already)
$ rails dev:cache
🔹 Step 2: Setup Model and Controller
// app/models/post.rb
class Post < ApplicationRecord
after_update_commit do
broadcast_replace_to self,
target: "post_#{id}",
partial: "posts/post",
locals: { post: self }
end
end
// app/controllers/posts_controller.rb
def update
@post = Post.find(params[:id])
if @post.update(post_params)
redirect_to posts_path, notice: "Updated successfully."
else
render :edit
end
end
🔹 Step 3: Render Cached Turbo Frame in View
// app/views/posts/_post.html.erb
<turbo-frame id="post_<%= post.id %>">
<% cache(post.cache_key_with_version) do %>
<div class="post-card">
<h3><%= post.title %></h3>
<p><%= post.body %></p>
<small>Updated: <%= post.updated_at.strftime("%H:%M:%S") %></small>
</div>
<% end %>
</turbo-frame>
🔹 Step 4: Render Post List
// app/views/posts/index.html.erb
<%= turbo_frame_tag "posts" do %>
<%= render @posts %>
<% end %>
🔹 Step 5: Setup View for Turbo Stream Response (optional)
If updating through an AJAX action manually (not after_commit), use Turbo Stream format explicitly.
// app/views/posts/update.turbo_stream.erb
<turbo-stream target="post_<%= @post.id %>" action="replace">
<template>
<%= render @post %>
</template>
</turbo-stream>
🔹 Step 6: Optional Debug – Show Timestamp
Add a timestamp to verify caching in development.
<% if Rails.env.development? %>
<div class="debug">Rendered at: <%= Time.current.to_s(:seconds) %></div>
<% end %>
Common Questions, Problems & Solutions
❓ Q1: Why isn’t my Turbo Stream reflecting changes?
Problem: You updated a record, but the broadcasted Turbo Stream still shows the old content.
Solution: Rails cached the partial. To fix this, ensure the cache key changes when the data changes — for example, by using `updated_at`.
// app/views/posts/_post.html.erb
<% cache [post, post.updated_at] do %>
<%= post.title %>
<% end %>
❓ Q2: How do I make sure associated changes bust the cache?
Problem: You update a comment on a post, but the post cache doesn’t change.
Solution: Use touch: true
in the association to update the parent timestamp.
// app/models/comment.rb
belongs_to :post, touch: true
❓ Q3: Why do I get a missing cache fragment error?
Problem: You’re trying to cache a partial that doesn’t exist or wasn’t correctly named.
Solution: Check that your partial path and cache key are both valid.
// Correct partial reference
<%= render partial: \"posts/post\", locals: { post: post } %>
❓ Q4: Why is my cache never hit?
Problem: You’re using a different key every time, so the cache always misses.
Solution: Use a stable key based on model attributes or use `cache_key_with_version`.
<% cache(post.cache_key_with_version) do %>
...content...
<% end %>
❓ Q5: How do I debug what’s cached or not?
Problem: It’s hard to tell if content is being served from the cache.
Solution: Use `Rails.logger.debug`, or temporarily show a timestamp or cache ID in the view.
<%= Time.current.to_s(:seconds) if Rails.env.development? %>
Alternative Methods or Concepts
- Use ETag + Conditional GET for full-page caching
- Use Redis or Memcached for fragment caching store
- Client-side localStorage/cache (for offline-first apps)
Best Practices with Examples
✅ 1. Use Stable Fragment Cache Keys
Use a stable cache key based on the object’s version or timestamp so cache invalidation happens correctly.
// Example using updated_at
<% cache [post, post.updated_at] do %>
<div class=\"post\"><%= post.title %></div>
<% end %>
// Or using cache_key_with_version
<% cache(post.cache_key_with_version) do %>
<div class=\"post\"><%= post.content %></div>
<% end %>
✅ 2. Use touch: true
for Nested Associations
When child models affect a parent’s display, use touch: true
to automatically update parent timestamps.
// app/models/comment.rb
belongs_to :post, touch: true
✅ 3. Cache Inside Turbo Frames
Scope your cache blocks inside <turbo-frame>
so the cached content can be dynamically replaced on updates.
// app/views/posts/_post.html.erb
<turbo-frame id=\"post_<%= post.id %>\">
<% cache(post) do %>
<div><%= post.title %></div>
<% end %>
</turbo-frame>
✅ 4. Broadcast Turbo Streams with Cached Partials
Use Turbo Stream broadcasts that render cached partials to keep updates fast and lightweight.
// app/models/post.rb
after_update_commit do
broadcast_replace_to self,
target: \"post_#{id}\",
partial: \"posts/post\",
locals: { post: self }
end
✅ 5. Preview Cache Status During Development
Use timestamps or version identifiers in your views temporarily to debug whether content is cached or freshly rendered.
<%= Time.current.to_s(:seconds) if Rails.env.development? %>
Real-World Scenario
In a blog platform, dozens of posts are updated frequently. Using Turbo Streams with Rails fragment caching allows each post to be updated independently via websockets without re-rendering the entire list. Cache versioning ensures stale content is never sent.
Detailed Explanation
Turbo improves UX by delivering fast, dynamic updates. However, since Turbo relies on JavaScript, bots and crawlers may not execute dynamic page changes. This means if your SEO content is injected via Turbo, it may not be indexed by search engines.
You need to ensure that critical content (like meta tags, titles, h1 headers, and canonical links) are present in full-page responses or properly handled in `turbo:load` events.
Implementation
Below is a complete example of how to structure your Rails + Turbo application for SEO-friendly pages. This includes metadata setup, dynamic updates, and full-page rendering for search engines.
🔹 Step 1: Setup Layout with SEO Placeholders
Update your layout to include dynamic title, description, and canonical tags.
// app/views/layouts/application.html.erb
<head>
<title><%= content_for?(:title) ? yield(:title) : "MyApp" %></title>
<meta name="description" content="<%= content_for?(:meta_description) ? yield(:meta_description) : 'Default SEO Description' %>">
<link rel="canonical" href="<%= request.original_url %>">
</head>
🔹 Step 2: Use `content_for` in Individual Views
Inject SEO content into views using content blocks.
// app/views/posts/show.html.erb
<% content_for :title, @post.title %>
<% content_for :meta_description, @post.summary || @post.excerpt %>
<h1><%= @post.title %></h1>
<p><%= @post.body %></p>
🔹 Step 3: Add Meta Tags Dynamically for Turbo Navigation
When navigating via Turbo, metadata is not reloaded — so we add hidden meta elements and update them on `turbo:load`.
// In view
<meta name="page-title" content="<%= @post.title %>">
<meta name="page-description" content="<%= @post.summary %>">
// app/javascript/controllers/seo_controller.js
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
connect() {
const title = document.querySelector("meta[name='page-title']")?.content;
const description = document.querySelector("meta[name='page-description']")?.content;
if (title) document.title = title;
if (description) {
const meta = document.querySelector("meta[name='description']")
if (meta) meta.setAttribute("content", description);
}
}
}
🔹 Step 4: Disable Turbo for SEO-Critical Pages
If a page needs full reload for SEO reasons, disable Turbo for those links.
// HTML link
<a href="/about" data-turbo="false">About Us</a>
🔹 Step 5: Force Full HTML for Bots (optional)
Detect search engine bots and skip Turbo navigation by default for them.
// app/controllers/application_controller.rb
before_action :disable_turbo_for_bots
def disable_turbo_for_bots
if request.user_agent =~ /bot|crawl|spider/i
request.headers["Turbo-Visit-Control"] = "reload"
end
end
Common Questions, Problems & Solutions
❓ Q1: Why doesn’t Google index my Turbo page content?
Problem: Googlebot may not execute JavaScript or follow Turbo navigation.
Solution: Ensure all core content is delivered via full-page loads, not just Turbo updates.
// In controller
def show
@post = Post.find(params[:id])
# Ensure standard full HTML response is available
end
❓ Q2: My title tag doesn’t update on Turbo navigation
Problem: Turbo does not automatically update the document’s <title> tag.
Solution: Use `content_for :title` and yield it in the layout, or manually update title via JavaScript on `turbo:load`.
// app/views/posts/show.html.erb
<% content_for :title, @post.title %>
// layout
<%= content_for?(:title) ? yield(:title) : \"Default Title\" %>
// app/javascript/application.js
document.addEventListener(\"turbo:load\", () => {
const newTitle = document.querySelector(\"meta[name='page-title']\")?.content;
if (newTitle) document.title = newTitle;
});
❓ Q3: My meta description doesn’t match my page content
Problem: Search engines crawl the initial HTML page but not the Turbo-injected content.
Solution: Use `content_for :meta_description` and render it in the layout server-side.
// app/views/layouts/application.html.erb
\">
❓ Q4: How can I stop Turbo for SEO-sensitive pages?
Problem: Some pages need full-page loads to be crawlable.
Solution: Use data-turbo=\"false\"
on anchor tags to opt-out of Turbo navigation.
<a href=\"/about\" data-turbo=\"false\">About Us</a>
❓ Q5: How do I avoid duplicate content with Turbo URLs?
Problem: URLs loaded via Turbo may result in duplicate content.
Solution: Use canonical tags in the head section to tell search engines the primary URL.
<link rel=\"canonical\" href=\"<%= request.original_url %>\" />
Alternative Methods or Concepts
- Use server-side rendering or pre-rendering for content-heavy pages
- Render static HTML snapshots for bots with tools like prerender.io
- Use a hybrid approach with Turbo disabled on SEO-sensitive routes
Best Practices with Examples
✅ 1. Use content_for
for Page Titles and Descriptions
Define meta tags and title in your layout using `content_for` so each view can override them per page.
// layout: app/views/layouts/application.html.erb
<title><%= content_for?(:title) ? yield(:title) : \"MyApp\" %></title>
<meta name=\"description\" content=\"<%= content_for?(:meta_description) ? yield(:meta_description) : \"Default description\" %>\">
// view: app/views/posts/show.html.erb
<% content_for :title, @post.title %>
<% content_for :meta_description, @post.summary %>
✅ 2. Add Canonical Tags to Avoid Duplicate Content
Search engines should know the “main” version of each page to avoid duplicate indexing.
// layout
<link rel=\"canonical\" href=\"<%= request.original_url %>\">
✅ 3. Prevent Turbo Navigation Where Needed
Pages that require full HTML loads for SEO should disable Turbo on links to them.
<a href=\"/pricing\" data-turbo=\"false\">Pricing Page</a>
✅ 4. Use JavaScript to Update Title/Meta as a Fallback
Turbo won’t update the <head> content — use JavaScript on turbo:load
to fill the gap for dynamic pages.
// Add hidden meta in view
<meta name=\"page-title\" content=\"<%= @post.title %>\">
// Update on page change
document.addEventListener(\"turbo:load\", () => {
const title = document.querySelector(\"meta[name='page-title']\")?.content;
if (title) document.title = title;
});
✅ 5. Use Server-Rendered Views for Crawlers
If necessary, detect crawlers and return fully-rendered pages or pre-rendered HTML snapshots.
// ApplicationController
before_action :disable_turbo_for_bots
def disable_turbo_for_bots
if request.user_agent =~ /bot|crawl|spider/i
request.headers[\"Turbo-Visit-Control\"] = \"reload\"
end
end
Real-World Scenario
An eCommerce Rails app uses Turbo for product filtering and detail views. To ensure product pages are indexed by search engines, it uses `content_for` in layouts to inject the proper page title and meta description. Turbo is disabled on some routes to preserve full-page loads for crawlers.
Detailed Explanation
Lazy-loading Turbo Frames allows you to defer rendering of non-critical or heavy components until after the initial page load. This can significantly improve perceived performance and speed up Time to Interactive (TTI), especially on pages with many sections.
Implementation
Below is a full implementation example of lazy-loading Turbo Frames in a Rails app to improve performance. It includes a controller action, a view with a placeholder, and the correct usage of partial rendering and caching.
🔹 Step 1: Add the Lazy Turbo Frame in Your Main View
// app/views/dashboard/index.html.erb
<h1>Dashboard Overview</h1>
<turbo-frame id="metrics" src="/dashboard/metrics" loading="lazy">
<div class="placeholder">Loading key metrics...</div>
</turbo-frame>
🔹 Step 2: Define the Controller Action
// app/controllers/dashboard_controller.rb
class DashboardController < ApplicationController
def index
# basic dashboard info, no heavy data here
end
def metrics
stats = expensive_data_lookup # simulate heavy stats query
render partial: "metrics", locals: { stats: stats }
end
end
🔹 Step 3: Create the Partial to Render
// app/views/dashboard/_metrics.html.erb
<% cache "dashboard-metrics" do %>
<div class="metric-box">
<h3>Revenue Today: <%= number_to_currency(stats[:revenue]) %></h3>
<p>New Signups: <%= stats[:signups] %></p>
</div>
<% end %>
🔹 Step 4: Add CSS for Smooth Loading
/* app/assets/stylesheets/application.css */
.placeholder {
background: #f8f9fa;
padding: 1rem;
text-align: center;
color: #999;
font-style: italic;
}
.metric-box {
background: #fff;
border: 1px solid #ddd;
border-radius: 8px;
padding: 1rem;
box-shadow: 0 2px 6px rgba(0,0,0,0.05);
}
🔹 Step 5: Optional Stimulus for Lazy Reload Button (Advanced)
// Add a refresh button inside the partial for manual updates
<button data-action="click->frame#reload" data-frame-id="metrics">Refresh Metrics</button>
// app/javascript/controllers/frame_controller.js
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
static values = { id: String }
reload(event) {
const frame = document.getElementById(this.element.dataset.frameId || this.idValue);
if (frame) frame.src = frame.src;
}
}
Common Questions, Problems & Solutions
❓ Q1: Why isn’t my lazy frame loading content?
Problem: The Turbo Frame shows the placeholder but never replaces it with the actual content.
Solution: Ensure your `src` path is correct and the controller action responds with a partial (not a layout).
// View
Loading user stats...
// Controller
def stats
@user = User.find(params[:id])
render partial: "users/stats", locals: { user: @user }
end
❓ Q2: Why does my frame reload the entire layout?
Problem: Turbo loads the frame content but includes the full layout, nesting layout inside your page.
Solution: Ensure your frame request renders a partial or uses `layout: false`.
render partial: "users/stats", layout: false
❓ Q3: Why do flash messages disappear after loading the frame?
Problem: Lazy-loaded frames don’t include flash rendering logic by default.
Solution: Keep flash message rendering outside of frames and in your main layout.
// layout.html.erb
<% if flash[:notice] %>
<%= flash[:notice] %>
<% end %>
❓ Q4: My placeholder flashes quickly even on fast loads. Can I hide it smoothly?
Problem: The loading indicator appears briefly even if the frame loads immediately.
Solution: Use a minimal placeholder or add a slight delay before showing it.
// View
Loading...
❓ Q5: Can I load multiple lazy frames in parallel?
Problem: Frames may seem to compete for requests or appear out of sync.
Solution: Yes — Turbo can handle parallel loading of multiple lazy frames efficiently. Just ensure unique IDs and endpoints.
// HTML
Alternative Methods or Concepts
- Stimulus controller to load on scroll or intersection observer
- Manual fetch using Fetch API and `innerHTML` injection
- Use pagination or tabs to conditionally load content
Best Practices with Examples
✅ 1. Use loading="lazy"
for non-critical or below-the-fold content
Lazy loading is perfect for widgets, sidebars, or sections like analytics or logs that don’t need to render immediately.
<turbo-frame id="metrics" src="/dashboard/metrics" loading="lazy">
<div class="loading">Loading metrics...</div>
</turbo-frame>
✅ 2. Always provide a fallback or loading message inside the frame
The content between the opening and closing frame tags serves as the placeholder while loading.
<turbo-frame id="comments" src="/posts/1/comments" loading="lazy">
<p>Fetching latest comments...</p>
</turbo-frame>
✅ 3. Use partials to return content — avoid full layouts in lazy frame responses
Lazy-loaded Turbo Frames should return HTML fragments, not full layout pages.
// app/controllers/dashboard_controller.rb
def metrics
render partial: \"metrics\", locals: { stats: compute_stats }
end
✅ 4. Cache lazy frame content when possible
Combine lazy-loading with Rails fragment caching for improved performance.
// app/views/dashboard/_metrics.html.erb
<% cache \"dashboard-metrics\" do %>
<div><%= stats[:revenue] %> today</div>
<% end %>
✅ 5. Avoid placing navigation or flash message rendering inside lazy frames
Turbo Frames isolate updates. Don’t include flash or layout elements that should be global.
// Bad: May hide flash message
<turbo-frame id="main" src="/something" loading="lazy">
<%= render \"layouts/flash\" %>
</turbo-frame>
Real-World Scenario
In an analytics dashboard, reports and charts are loaded in turbo-frames with `loading=\”lazy\”`. This ensures the page loads instantly with key metrics, while heavy chart data loads quietly in the background — giving users a faster experience and reducing backend pressure.
Detailed Explanation
Hotwire is a modern approach to building web applications without writing much JavaScript. It’s powered by three main components: Turbo Drive, Turbo Frames, and Turbo Streams. Under the hood, Hotwire intercepts links and form submissions, fetches updated HTML using Fetch/XHR, and replaces content inside `<turbo-frame>` or updates DOM elements using `<turbo-stream>` tags.
Instead of sending JSON and handling it on the client, Hotwire allows the server to return HTML responses that get applied directly to the DOM. This provides real-time updates without full-page reloads.
Common Questions, Problems & Solutions
❓ Q1: Why doesn’t my Turbo Frame update after form submission?
Problem: You submit a form inside a Turbo Frame, but the frame content doesn’t update.
Solution: Ensure that the server response includes a partial wrapped in the same Turbo Frame ID.
// View
<%= form_with model: @user do |f| %>
<%= f.text_field :name %>
<% end %>
// Controller
if @user.save
render partial: "users/success", locals: { user: @user }
else
render partial: "users/form", locals: { user: @user }
end
❓ Q2: Why are my Turbo Stream broadcasts not triggering changes?
Problem: You broadcast a Turbo Stream but nothing happens in the UI.
Solution: Make sure the client is subscribed to the correct stream via `broadcasts_to`, and the `.turbo_stream.erb` format is being rendered.
// model
class Comment < ApplicationRecord
broadcasts_to :post
end
// turbo_stream view
<%= render @comment %>
❓ Q3: How do I debug which frame or stream failed?
Problem: Nothing updates, and there’s no visible error.
Solution: Open DevTools and check the Network tab for the Turbo request. Make sure:
- The response has correct Turbo syntax (Frame or Stream tags)
- The Content-Type is
text/vnd.turbo-stream.html
ortext/html
// Optional logging
document.addEventListener("turbo:before-fetch-response", (e) => {
console.log("Turbo Response:", e.detail.fetchResponse.response);
});
❓ Q4: Can I send multiple Turbo Streams in one response?
Problem: You want to update several elements at once (e.g., a list + a counter).
Solution: Render multiple `
// update.turbo_stream.erb
<%= render @comment %>
<%= @post.comments.count %> Comments
❓ Q5: Can I make my own version of Hotwire?
Problem: You want custom real-time updates, but without using Hotwire.
Solution: Yes! At a basic level, you can:
- Use ActionCable or WebSockets to listen for updates
- Send HTML from the server
- Use JS to insert it with
innerHTML
orreplaceChild
// JS
cable.subscriptions.create(\"RoomChannel\", {
received(data) {
document.getElementById(\"updates\").innerHTML = data.html;
}
});
Alternative Methods or Concepts
- Phoenix LiveView (Elixir)
- StimulusReflex (Rails)
- React with REST or GraphQL for client-side rendering
Best Practices with Examples
✅ 1. Keep Turbo Frames Small and Focused
Isolate interactive elements (like forms, sections, or buttons) inside their own Turbo Frames. This reduces the amount of HTML replaced and improves UX.
// Good practice
<%= render "form", comment: @comment %>
✅ 2. Always Match Turbo Frame ID in HTML and Server Response
Turbo only replaces the frame if the incoming HTML has a matching `turbo-frame id`.
// View
<%= render "form" %>
// Server response must include:
...updated HTML...
✅ 3. Use Turbo Streams for Real-Time UX (not just navigation)
Broadcast changes via Turbo Streams to update UI without user interaction.
// app/models/comment.rb
class Comment < ApplicationRecord
broadcasts_to :post
end
// app/views/comments/create.turbo_stream.erb
<%= render @comment %>
✅ 4. Leverage Stimulus for Custom Interactions
Use StimulusJS for any dynamic behavior like character counters, tab switching, or preview updates — instead of writing jQuery or raw JS.
// app/javascript/controllers/preview_controller.js
update() {
this.outputTarget.innerText = this.inputTarget.value;
}
✅ 5. Keep Stream Updates Lightweight
Avoid rendering full layouts in Turbo Stream responses — render small partials only.
// Bad: full layout rendering
render :show
// Good: partial only
render partial: "comment", locals: { comment: @comment }
Real-World Scenario
In a social media Rails app, user posts and comments are streamed in real-time using Turbo Streams. Users can reply and update their profiles inline using Turbo Frames without leaving the page or writing JavaScript.
Detailed Explanation
Hotwire is composed of three core components that work together to deliver fast, real-time web applications without writing much JavaScript:
- 1. Turbo Drive:
Automatically intercepts normal link clicks and form submissions to avoid full-page reloads.
It uses fetch API under the hood to swap the body of the document using
pushState
.
Example: Clicking a link reloads only the `` without a full page refresh. - 2. Turbo Frames:
Allow partial updates by targeting specific sections of the page. They isolate a part of the DOM, and only that section is replaced.
Example:<turbo-frame id=\"profile\" src=\"/users/1/edit\">Loading...</turbo-frame>
- 3. Turbo Streams:
Provide real-time updates over WebSockets or long polling. They use actions like
append
,prepend
,replace
, and more.
Example:<turbo-stream action=\"append\" target=\"comments\"> <template> <%= render @comment %> </template> </turbo-stream>
- 4. Stimulus: A small JavaScript framework that complements Turbo by enhancing user interactions (like modals, dropdowns, previews).
Together, these tools make up Hotwire. Turbo handles navigation and updates, while Stimulus adds behavior. The result is a fast, interactive frontend without the complexity of a full JavaScript framework.
Detailed Explanation (Easy Version)
In traditional Rails apps, clicking a link or submitting a form reloads the **entire page** from the server. This includes fetching HTML, CSS, JS, and re-rendering the entire browser view.
Turbo Drive changes this by making the navigation feel instant. It:
- Intercepts link clicks and form submits
- Loads the new page via
fetch()
instead of full reload - Only replaces the `` content — not the entire document
- Preserves layout, header, and scripts already loaded
🔁 Traditional Navigation
- Full-page reload
- All CSS/JS reloaded
- Slower user experience
⚡ Turbo Drive Navigation
- Fast navigation (like a single-page app)
- HTML is streamed and body content updated
- JavaScript and assets are preserved
🧪 Example:
// Standard Link
<a href=\"/products\">Products</a> ← reloads full page
// Turbo Link (no change in HTML needed)
<a href=\"/products\">Products</a> ← Turbo intercepts and swaps body
Note: Turbo Drive is enabled by default in Rails 7 via `import \"@hotwired/turbo-rails\"`
Detailed Explanation (Simple & Easy)
When you use a <turbo-frame>
and it sends a request to the server (like via a link click or form submission inside the frame), here's what happens behind the scenes:
- Turbo sends an AJAX request to the URL in the
src
or formaction
. - The server responds with a small HTML fragment.
- Turbo looks for a matching
<turbo-frame id=\"same_id\">
in the response. - It replaces the content inside the existing frame with the new HTML.
⚠️ Important: The response must include the exact same frame id
as the one requesting it.
🧪 Example:
// In your view
<turbo-frame id=\"user_form\" src=\"/users/1/edit\">
Loading user...
</turbo-frame>
// Server response
<turbo-frame id=\"user_form\">
<form action=\"/users/1\" method=\"post\">...</form>
</turbo-frame>
✅ Turbo automatically swaps the contents inside the frame on the page, without refreshing the whole page.
This makes your app feel faster, like a single-page app, with less JavaScript and less complexity.
Detailed Explanation (Simple & Practical)
Turbo Streams allow you to **update parts of your HTML in real-time** from the server using ActionCable (WebSockets) or HTTP responses. They use small HTML snippets to tell the browser *what* to change and *where*.
🔄 Imagine you submit a comment, and it instantly appears for everyone — that's Turbo Streams!
🧪 How It Works:
- Your Rails model broadcasts a change (e.g., a new comment).
- The server sends a special `
` HTML tag with the update. - Turbo reads the tag and applies the update to your page — without any JavaScript!
✅ Available Actions:
- append – Add new content at the end of a target element
- prepend – Add new content at the beginning
- replace – Replace an existing element
- update – Replace only the inner content of a target element
- remove – Remove an element from the DOM
💡 Example:
<turbo-stream action=\"append\" target=\"comments\">
<template>
<div id=\"comment_123\">New comment!</div>
</template>
</turbo-stream>
📦 Example Model Setup:
class Comment < ApplicationRecord
broadcasts_to :post
end
That’s it! Now when you create a new comment, Turbo will stream the update live to any connected users.
Detailed Explanation (Simple & Practical)
Turbo Streams can automatically broadcast real-time updates from your Rails model using ActionCable (WebSockets). This means when a record is created, updated, or destroyed, it can notify all connected users to update their views instantly.
🛠️ Step-by-Step Example
1. Add broadcasts_to
in your model:
// app/models/comment.rb
class Comment < ApplicationRecord
belongs_to :post
broadcasts_to :post
end
This will automatically create Turbo Stream broadcasts for the associated post.
2. Add a Turbo Stream target in the view:
<div id=\"comments\">
<%= turbo_stream_from @post %>
<%= render @post.comments %>
</div>
3. Create a partial for each comment:
// app/views/comments/_comment.html.erb
<div id=\"comment_<%= comment.id %>\">
<p><%= comment.body %></p>
</div>
4. Turbo will now send:
- 📤
append
stream when a comment is created - 🔁
replace
stream when a comment is updated - ❌
remove
stream when a comment is destroyed
🎉 Result:
Users viewing the page will automatically see new comments appear in real time, without any JavaScript!
Detailed Explanation (Simple & Practical)
When using Turbo Frames in Rails, you can handle form errors inline — without reloading the entire page — by keeping the form inside a frame and re-rendering it when validation fails.
🧠 How It Works:
- You place the form inside a
<turbo-frame>
- On submission, Turbo sends the form via AJAX
- If there are validation errors, the controller re-renders the form partial (with error messages)
- Turbo swaps only the contents inside the frame
🧪 Example:
// View: new.html.erb or edit.html.erb
<turbo-frame id=\"user_form\">
<%= render \"form\", user: @user %>
</turbo-frame>
// Partial: _form.html.erb
<%= form_with model: user, data: { turbo_frame: \"user_form\" } do |f| %>
<%= f.text_field :name %>
<%= f.submit %>
<% if user.errors.any? %>
<div class=\"errors\">
<% user.errors.full_messages.each do |msg| %>
<p><%= msg %></p>
<% end %>
</div>
<% end %>
<% end %>
// Controller: users_controller.rb
def create
@user = User.new(user_params)
if @user.save
redirect_to @user
else
render partial: \"form\", locals: { user: @user }, status: :unprocessable_entity
end
end
✅ Result:
The form is updated inside the frame only. Errors are shown instantly, no full-page reload is needed.
Detailed Explanation (Simple & Practical)
While Turbo improves speed and interactivity, it comes with some important limitations. Let’s break them down in a simple way:
🔍 1. SEO Limitations
Turbo pages load dynamically using JavaScript. Most search engines **expect full HTML** (server-rendered pages), and may not index content inside Turbo Frames unless fallback routing is provided.
Workaround: Use `data-turbo=\"false\"` for important pages like blogs or landing pages so they render full-page views.
<a href=\"/pricing\" data-turbo=\"false\">See Plans</a>
⚙️ 2. JavaScript Execution
When Turbo replaces part of a page (like a Turbo Frame or stream), **inline scripts inside the response do not run automatically**.
Workaround: Use StimulusJS controllers or manually trigger JS inside turbo:load
or turbo:frame-load
events.
// Example fallback using event listener
document.addEventListener(\"turbo:frame-load\", () => {
console.log(\"JS triggered after frame loaded\");
});
📦 3. Nested Turbo Frames
Turbo does not support **nested frames** (a frame inside another frame). If you nest them, only the outer frame will respond properly.
Workaround: Flatten frame structure or dynamically load one level at a time.
// ❌ Avoid:
<turbo-frame id=\"outer\">
<turbo-frame id=\"inner\">...</turbo-frame>
</turbo-frame>
📌 Summary:
- ✅ Use Turbo where interactivity is important
- ⛔ Avoid for pages that depend on SEO
- ⚠️ JavaScript in dynamic content needs event-based triggering
- ⚠️ Avoid nesting frames unless necessary
Detailed Explanation (Easy to Understand)
Turbo Streams let you use default actions like append
, replace
, etc.
But you can also create **your own custom actions**, like fade_in
or scroll_to
— to add effects or behavior to updates.
🛠️ How to Implement a Custom Action
1. Register the action in JavaScript:
// app/javascript/controllers/custom_actions.js
import { Turbo } from \"@hotwired/turbo-rails\"
Turbo.StreamActions.fade_in = (target, element) => {
target.innerHTML = element.innerHTML;
target.style.opacity = 0;
target.style.transition = \"opacity 0.6s ease\";
requestAnimationFrame(() => {
target.style.opacity = 1;
});
};
Turbo.StreamActions.scroll_to = (target) => {
target.scrollIntoView({ behavior: \"smooth\", block: \"start\" });
};
2. Use your action in a `.turbo_stream.erb` file:
// example.turbo_stream.erb
<turbo-stream action=\"fade_in\" target=\"message_123\">
<template>
<div id=\"message_123\">New message!</div>
</template>
</turbo-stream>
<turbo-stream action=\"scroll_to\" target=\"message_123\" />
📦 Result:
✅ The element updates with a smooth fade-in effect.
✅ The page scrolls to the new content automatically.
💡 Tip:
You can define any action that modifies the DOM using Turbo.StreamActions.YOUR_ACTION
.
Detailed Explanation (Simple & Practical)
Stimulus is a small JavaScript framework that works with HTML — it gives your pages behavior and interactivity. In a Hotwire setup, Turbo handles navigation and updates, while Stimulus adds dynamic interactions.
🧠 Think of it like this:
- Turbo: Manages where and how content updates on your page.
- Stimulus: Manages what happens *after* the content is there — like buttons, previews, dropdowns, etc.
🧪 Example: Live Character Counter
// HTML (with Stimulus controller)
<div data-controller=\"counter\">
<input data-counter-target=\"input\" data-action=\"input->counter#update\" />
<span data-counter-target=\"output\">0</span>
</div>
// app/javascript/controllers/counter_controller.js
import { Controller } from \"@hotwired/stimulus\"
export default class extends Controller {
static targets = [\"input\", \"output\"]
update() {
this.outputTarget.textContent = this.inputTarget.value.length
}
}
💡 Where Stimulus Helps in Hotwire
- ✅ After a Turbo Frame loads: Run JS logic like highlighting or form resets
- ✅ Inside Turbo Stream templates: Add custom animation, validations, etc.
- ✅ When enhancing UX: Like modal toggles, live previews, or rich text editors
📦 Summary:
Turbo gives you structure, but Stimulus gives you behavior. Together, they let you build modern web apps using just HTML and a little JavaScript.
Detailed Explanation (Simple & Practical)
When a Turbo Frame or Stream doesn’t work, the problem is usually with the HTML structure, response format, or JavaScript event timing. Here's how to debug it step by step.
🔍 Step-by-Step Debug Tips
✅ 1. Check the Frame or Stream id
The id
in your response must match the <turbo-frame>
or the stream target.
// View
<turbo-frame id=\"user_profile\"></turbo-frame>
// Response must include
<turbo-frame id=\"user_profile\">...updated content...</turbo-frame>
✅ 2. Inspect the Network Tab
Open your browser DevTools → Network tab → check the request and response of your frame or stream. Make sure:
- Response contains correct HTML
- Content-Type is
text/html
ortext/vnd.turbo-stream.html
✅ 3. Use Turbo Events to Log Issues
// Use this in browser console or JS file
document.addEventListener(\"turbo:before-fetch-response\", (event) => {
console.log(\"Turbo Response:\", event.detail.fetchResponse.response);
});
document.addEventListener(\"turbo:frame-missing\", (e) => {
console.warn(\"Missing Turbo Frame:\", e.target.id);
});
✅ 4. Validate Your Partial Rendering
Turbo Frames and Streams should only return the specific partial — not the full layout.
// Good
render partial: \"form\", locals: { user: @user }
// Bad (causes layout nesting issues)
render :edit
✅ 5. Streams Not Working? Check MIME Type
Turbo Streams must return text/vnd.turbo-stream.html
. Use:
// In controller
render turbo_stream: turbo_stream.append(\"comments\", partial: \"comments/comment\", locals: { comment: @comment })
🧠 Summary
- ✅ Always match IDs
- ✅ Check the browser network tab
- ✅ Listen to Turbo events for more clues
- ✅ Make sure you’re rendering partials, not full pages
you’re in reality a just right webmaster. The site loading speed is amazing. It seems that you are doing any distinctive trick. Also, The contents are masterpiece. you’ve performed a excellent process on this topic!
You got a very fantastic website, Glad I discovered it through yahoo.