ViewComponent/view_component · error · ViewComponent::Serializable::UnserializableError

Cannot serialize slot call '#{method_name}' with a block

Error message

Cannot serialize slot call '#{method_name}' with a block

What it means

Slot calls on a render_later Proxy are captured by method_missing and replayed at render time, but only as (method, args) pairs stored in slot_calls — a Ruby block cannot be captured that way. capture_slot_call therefore raises ViewComponent::Serializable::UnserializableError ("Cannot serialize slot call 'with_x' with a block") when a with_* slot method is invoked with a block on the proxy. Slot content must be passed as serializable arguments.

Source

Thrown at lib/view_component/serializable/proxy.rb:94

      end

      if defined?(Rails::VERSION) && Rails::VERSION::MAJOR == 7 && Rails::VERSION::MINOR == 1
        # Rails expects us to define `format` on all renderables,
        # but we do not know the `format` of a ViewComponent until runtime.
        def format
          nil
        end
      end

      private

      def slot_method?(method_name)
        method_name.to_s.start_with?("with_") && @component_class.method_defined?(method_name)
      end

      def capture_slot_call(method_name, args, &block)
        if block
          raise UnserializableError, "Cannot serialize slot call '#{method_name}' with a block"
        end

        @slot_calls << {method: method_name, args: args}
        self
      end

      def build_component
        @component_class.new(*@initialize_args).tap do |instance|
          @slot_calls.each do |call|
            instance.public_send(call[:method], *call[:args])
          end
        end
      end
    end
  end
end

View on GitHub (pinned to 9f22c36fa7)

Solutions

  1. Drop the block and pass the content as an argument the slot accepts: .with_item(label: "One", body: "Body text").
  2. If the slot normally takes a block, redesign it to accept content as plain data (or a nested deferred component) rather than a block.
  3. If block semantics are required, build and render the real component synchronously instead of the proxy.

Example fix

# before
proxy = MyComponent.render_later("title").with_item(label: "One") do
  "<span>Block body</span>"
end # UnserializableError on slot call

# after
proxy = MyComponent.render_later("title").with_item(label: "One", body: "<span>Block body</span>")
Defensive patterns

Strategy: validation

Validate before calling

def with_serializable_slot(proxy, method_name, *args, &block)
  raise ArgumentError, "slot calls on a deferred proxy cannot take blocks" if block
  proxy.public_send(method_name, *args)
end

Prevention

When it happens

Trigger: MyComponent.render_later(...).with_item { "content" }; calling with_item(label: "One") { |item| ... } on the proxy; porting block-style slot usage from a concrete component to the deferred proxy.

Common situations: Components whose slots normally receive blocks in templates (renders_many :items with block content); moving Turbo Stream renders into jobs while keeping block-based slot calls; helper methods that always attach a block to slot setters.

Related errors


AI-assisted analysis of ViewComponent/view_component@9f22c36fa7 (2026-08-23). Data as JSON: /api/errors/b6d22d3ca3f6241a. Report an issue: GitHub.