Turbo Streams
ViewComponents can be used with Turbo Streams to broadcast updates over WebSockets.
Rendering in controllers
In a controller action, render a component inside a Turbo Stream response using render_to_string or view_context.render:
class MessagesController < ApplicationController
def create
@message = Message.create!(message_params)
Turbo::StreamsChannel.broadcast_append_to(
"messages",
target: "messages",
html: render_to_string(MessageComponent.new(message: @message))
)
end
end
Broadcasting later with ActiveJob
To broadcast asynchronously via ActiveJob (using broadcast_action_later_to), components must be serializable so they can be passed to the background job.
Include ViewComponent::Serializable in the component and use .render_later instead of .new when broadcasting:
class MessageComponent < ViewComponent::Base
include ViewComponent::Serializable
def initialize(message:)
@message = message
end
end
class Message < ApplicationRecord
after_create_commit :broadcast_append
private
def broadcast_append
Turbo::StreamsChannel.broadcast_action_later_to(
"messages",
action: :append,
target: "messages",
renderable: MessageComponent.render_later(message: self),
layout: false
)
end
end
The component is serialized when the job is enqueued and deserialized when the job runs. The job renders the component via ApplicationController.render and broadcasts the resulting HTML over ActionCable.
Slots
Slot calls made on the proxy are captured and replayed at render time. Slots that accept blocks aren’t supported because blocks can’t be serialized.
class CardComponent < ViewComponent::Base
class BodyComponent < ViewComponent::Base
def initialize(text:)
@text = text
end
end
renders_many :bodies, BodyComponent
end
proxy = CardComponent.render_later(title: "Updates")
proxy.with_body(text: "First item")
proxy.with_body(text: "Second item")
Turbo::StreamsChannel.broadcast_action_later_to(
"cards",
renderable: proxy,
action: :append,
target: "cards",
layout: false
)
Passing a block to a slot call raises ViewComponent::Serializable::UnserializableError immediately.
How it works
.render_later(*args, **kwargs)returns aViewComponent::Serializable::Proxythat stores the component class and initialization arguments without instantiating the component.ViewComponent::ActiveJobSerializerhandles converting the proxy to and from a JSON-safe format. It’s automatically registered when ActiveJob is loaded.- ActiveRecord objects passed as arguments are serialized via GlobalID, just like any other ActiveJob argument.
Limitations
- Blocks passed to
render_later,render_in, or slot calls raiseViewComponent::Serializable::UnserializableError. Use component-based slots instead. - The component class must be named and loadable so it can be resolved via
safe_constantizeat deserialization time.render_lateron an anonymous class raisesViewComponent::Serializable::UnserializableError.