Skip to content

Conversation

@CyberAP
Copy link

@CyberAP CyberAP commented Jan 31, 2026

What are you trying to accomplish?

This PR adds support for slot arguments.

# greeting_component.rb
class GreetingComponent < ViewComponent::Base
  renders_one :message
end
<%# greeting_component.html.erb %>
<div class="greeting">
  <%= message("Hello", "World") %>
</div>
<%# index.html.erb %>
<%= render GreetingComponent.new do |component| %>
  <% component.with_message do |greeting, name| %>
    <%= greeting %>, <%= name %>!
  <% end %>
<% end %>

Returning:

<div class="greeting">
  Hello, World!
</div>

What approach did you choose and why?

Slot arguments provide 2 major benefits to a component approach:

  1. Isolation – parent components no longer need to access private component instance to grab necessary data for rendering
  2. Configuration – slots could be used in more sophisticated ways previously not possible

Right now ViewComponent only supports basic slots without arguments. This means you have to follow patterns like this in order to get the data you need for rendering:

<%# index.html.erb %>
<%= render GreetingComponent.new do |component| %>
  <% component.with_message %>
    <%= component.greeting %>, <%= component.name %>!
  <% end %>
<% end %>

This breaks one of the key component principles: Encapsulation. If the parent component knows about internal state of the child component they become coupled. That leads to unreliable refactorings and degraded component's reusability.

A common pattern of customizing a slot would be to use lambda slots:

class BlogComponent < ViewComponent::Base
  renders_one :header, ->(classes:, &block) do
    content_tag :h1, class: classes, &block
  end
end

Unfortunately this approach does not allow us to pass data to the block directly, meaning it's actually the inverse of what we want: it passes data to a child component, not the child component passing data to the parent.

Another problem with this approach is that we can't customize what exactly we render inside the lambda.

Slot arguments allow us to solve these problems with elegance. Instead of component instance we access slot arguments passed to the block:

<%# index.html.erb %>
<%= render GreetingComponent.new do |component| %>
  <% component.with_message do |greeting, name| %>
    <%= greeting %>, <%= name %>!
  <% end %>
<% end %>

This means we are in full control of what's rendered inside the slot and also never touch component internals, allowing for safer refactorings.

With slot arguments we can now support more advanced cases of using slots:

<%# greeting_component.html.erb %>
<div class="greetings">
  <% greetings.each_with_index do |greeting, index| %>
    <%= message(greeting, index) %>
  <% end %>
</div>
<%# index.html.erb %>
<%= render GreetingComponent.new do |component| %>
  <% component.with_message do |greeting, index| %>
    <%= index % 2 == 0 ? greeting : '' %>!
  <% end %>
<% end %>

Anything you want to highlight for special attention from reviewers?

This feature has been previously discussed but there still is no working alternative to it.

This approach is not new and is inspired by Scoped Slots in Vue and Child Component as Function pattern in React.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant