But It Works on My Machine!

View Components Over Turbo Streams with Hotwire


TL;DR:
You can render ViewComponents over Turbo Streams instead of partials. This provides: Simply replace partial: "..." with renderable: YourComponent.new(...) in your turbo stream actions.

Remember when Hotwire launched in December 2020? It feels like it was just yesterday. The video on Hotwire demonstrating this new Rails feature (using a chat that sends messages over the wire as an example) was amazing, and I fell in love with it instantly.

Time has passed (almost five years at the time of writing!), and the Hotwire ecosystem now includes many new features. However, there is one feature that isn’t widely known (introduced in 2023), and I want to share it with you today. It will likely make your life easier and provide benefits that I personally find awesome.

Let's start with a quick recap.

Turbo Streams 101

Turbo Streams allow Rails applications to push updates to the client over WebSockets (or any ActionCable channel) without requiring a full page reload. A typical broadcast looks like this:

Turbo::StreamsChannel.broadcast_append_to(
  "chat_room_1",
  target: "messages",
  partial: "messages/message",
  locals: { message: @message }
)

Here, Rails renders the message partial for a new message and sends the HTML over the wire. Simple, effective—but it has a downside.

The Problem with Partials

Partials are quick to write, but as your app grows, they can become difficult to maintain:

This is where ViewComponents come in.

Introducing ViewComponents

ViewComponent is a library that encourages encapsulation of view logic into reusable, testable Ruby classes:

class MessageComponent < ViewComponent::Base
  def initialize(message:)
    @message = message
  end
end

You can then render the component in your views:

<%= render(MessageComponent.new(message: @message)) %>

Benefits:

Rendering Components Over Turbo Streams - an EASY change!

Turbo::StreamsChannel.broadcast_append_to(
  "chat_room_1",
  target: "messages",
  renderable: MessageComponent.new(message: @message)
)

Here’s what happens:

This approach gives you all the advantages of ViewComponents while keeping the real-time behavior of Turbo Streams.

Check demo at Stremeable

Why It’s Beneficial

Performance: Rendering components is often faster than partials, especially for complex views or large collections. Components can avoid repeated template lookups and make heavy use of Ruby’s object-oriented optimizations.

Maintainability: Components encapsulate logic, so your views stay clean. Updating how messages are displayed requires changing only the component, not multiple partials across the app.

Testability: You can write unit tests for components rendering under different conditions. This is much harder with partials that rely on complex controller contexts.

Consistency Across UI: The same component can be used in Turbo Streams, regular views, emails, or previews. This ensures UI consistency and reduces duplication.

If you want to try it yourself, here is the repo: Hotwire Chat using ViewComponents where I did a quick demo of message rendering using ViewComponents.

Benchmarks

Message rendering benchmark

RAILS_ENV=production bin/rails runner benchmarks/messages_benchmark.rb
Running benchmark on 100 messages...
ruby 3.4.6 (2025-09-16 revision dbd83256b1) +PRISM [x86_64-linux]
Warming up --------------------------------------
             partial    53.000 i/100ms
           component   111.000 i/100ms
Calculating -------------------------------------
             partial    531.172 (± 2.4%) i/s    (1.88 ms/i) -      5.353k in  10.083752s
           component      1.070k (± 4.5%) i/s  (934.93 μs/i) -     10.767k in  10.088036s

Comparison:
           component:     1069.6 i/s
             partial:      531.2 i/s - 2.01x  slower

Turbo stream append benchmark

RAILS_ENV=production bin/rails runner benchmarks/turbo_streams_benchmark.rb
Preparing Turbo Stream benchmark with 100 messages...
Running Turbo Stream benchmark...
ruby 3.4.6 (2025-09-16 revision dbd83256b1) +PRISM [x86_64-linux]
Warming up --------------------------------------
turbo_stream partial    14.000 i/100ms
turbo_stream component  22.000 i/100ms
Calculating -------------------------------------
turbo_stream partial    143.450 (± 4.2%) i/s    (6.97 ms/i) -      1.442k in  10.069179s
turbo_stream component  219.430 (± 4.1%) i/s    (4.56 ms/i) -      2.200k in  10.045082s

Comparison:
turbo_stream component:      219.4 i/s
turbo_stream partial:      143.5 i/s - 1.53x  slower

Conclusion

Switching from partials to ViewComponents in Turbo Streams is a small change that pays off big: faster rendering, cleaner code, easier testing, and more reusable UI elements. Let me know what you think about it. Thanks for reading! :)

#hotwire #maintainability #performance #rails #ruby #testability #turbo-streams #viewcomponents #web-development