Work with our skilled Ruby on Rails developers to accelerate your project and boost its performance.
Hire Ruby on Rails DeveloperWhen working with Hotwire in a Ruby on Rails application, especially when combining turbo_frame_tag and ViewComponents, you may run into a strange issue: Turbo Frames not replacing content as expected. Instead of updating the frame, Rails renders a duplicate frame at the top of the DOM, even outside thetag.
If you’re transitioning from Vue.js or another front-end framework, this can be especially frustrating. Let’s dive into why this happens and how to fix it.
In a typical Rails + Hotwire setup, you might have:
The expectation is that submitting the form will update the messages list inside the turbo frame.
Instead of replacing the frame’s content:
You may have even checked the frame IDs and confirmed that they match. So what’s wrong?
Let’s talk about how Hotwire determines what to do with a response.
When the browser sends the request, it includes an Accept header like this:
Arduino:
text/vnd.turbo-stream.html, text/html
Rails looks at this header to decide what format your response should be. If it picks text/vnd.turbo-stream.html, it expects your response to include tags. If you’re simply rendering a ViewComponent, there’s no turbo stream wrapper, so Turbo doesn’t know what to do.
As a result, it dumps the HTML content into the DOM without any context, hence the weird behavior.
To make sure Turbo replaces the frame content correctly, you need to respond with standard HTML, not with Turbo Stream format.
def create message = current_user.messages.create(message_params) respond_to do |format| format.html do render Atoms::MessagesComponent.new(messages: message.owner.messages) end end end
This tells Rails to respond using HTML format, ensuring the output is valid HTML, not Turbo Stream markup
If you’re not using a respond_to block, you can set the content type directly:
render Atoms::MessagesComponent.new(messages: message.owner.messages), content_type: "text/html"
This is straightforward and ensures that the response is treated as plain HTML.
If you always want a particular component to return HTML (and you’re using ViewComponent), you can define the format method:
class Atoms::MessagesComponent < ViewComponent::Base def format :html end end
This ensures that every time the component is rendered, the content type will default to HTML, avoiding the Turbo Stream confusion altogether.
When Turbo receives a response with text/vnd.turbo-stream.html but doesn’t see a
Since there’s no instruction on how to apply the update (e.g., replace, append, prepend), Turbo assumes it’s a no-op and your new content appears outside of
, unstyled, and in the wrong place.To make Turbo Frames behave correctly when using ViewComponents:
Rails + Hotwire is powerful, but mixing it with ViewComponents requires attention to response formats. If your Turbo Frames aren't behaving as expected, inspect the response headers and make sure you're sending valid HTML, not Turbo Stream format, unless you're actually using
By controlling the response format, you can ensure seamless updates in your Turbo-powered UI.