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 a ViewComponent::Serializable::Proxy that stores the component class and initialization arguments without instantiating the component.
  • ViewComponent::ActiveJobSerializer handles 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 raise ViewComponent::Serializable::UnserializableError. Use component-based slots instead.
  • The component class must be named and loadable so it can be resolved via safe_constantize at deserialization time. render_later on an anonymous class raises ViewComponent::Serializable::UnserializableError.