Need Help With Ruby On Rails Development?

Work with our skilled Ruby on Rails developers to accelerate your project and boost its performance.

Hire Ruby on Rails Developer

Support On Demand!

When 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.

The Problem: Frame Duplication Instead of Replacement

In a typical Rails + Hotwire setup, you might have:

  • A turbo_frame_tag that wraps a list of messages.
  • A form outside of this frame that submits new messages via form_with targeting the same frame.

The expectation is that submitting the form will update the messages list inside the turbo frame.

But what happens?

Instead of replacing the frame’s content:

  • A new turbo frame is rendered (with the same ID).
  • It ends up outside of the body tag, usually at the top of the document.
  • The content isn’t replaced inside the original frame.

You may have even checked the frame IDs and confirmed that they match. So what’s wrong?

The Root Cause: Turbo Streams Expect Turbo Stream Markup

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.

The Fix: Force the Response Format to HTML

To make sure Turbo replaces the frame content correctly, you need to respond with standard HTML, not with Turbo Stream format.

Option 1: Use respond_to Block

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

Option 2: Manually Set Content Type

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.

Option 3: Set the Default Format in Your Component

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.

Why the Frame Appears Outside the Body

When Turbo receives a response with text/vnd.turbo-stream.html but doesn’t see a tag, it simply appends the raw HTML to the top of the document.

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.

Recap: Solving Turbo Frame Replacement Issues

To make Turbo Frames behave correctly when using ViewComponents:

  • Always ensure the response format is HTML if you’re not using Turbo Streams.
  • Use respond_to, content_type: "text/html", or define the format method.
  • Never assume that matching frame IDs is enough. Turbo also needs the correct response structure.

Final Thoughts

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 tags.

By controlling the response format, you can ensure seamless updates in your Turbo-powered UI.

Related Q&A