Component Composition
When to Use
Use this when converting React patterns for combining components -- nesting, polymorphic elements, higher-order components, context providers, and compound component APIs.
Decision
| React Pattern | Twig/SDC Equivalent | Example |
|---|---|---|
<Parent><Child /></Parent> |
Slots (named blocks) | Parent defines slots; child content fills them |
<Component as="section"> |
Dynamic tag: {% set tag = tag\|default('div') %} then <{{ tag }}> |
Polymorphic element |
<Component as={Link}> |
Conditional wrapping with {% if url %}<a>{% else %}<div>{% endif %} |
Cannot pass component types; use conditional tags |
React.cloneElement(child, extraProps) |
No equivalent | Pass data via include/embed context instead |
HOC: withAuth(Component) |
Preprocess function | Access control and data injection happen in PHP preprocess |
forwardRef |
Not needed | Drupal's attributes variable passes through all HTML attributes naturally |
| Context/Provider | Preprocess variables or Twig globals | Data flows from PHP to Twig; no client-side context tree |
Compound: <Tabs><Tab /><TabPanel /></Tabs> |
Single SDC with slots, or parent + include for items | Flatten into one component with an items slot |
Portal: createPortal(<Modal />, body) |
Render in Drupal region or use <dialog> |
No DOM teleportation; use page-level regions |
Pattern: Dynamic HTML Tag (Polymorphic Component)
React
function Box({ as: Tag = 'div', className, children, ...rest }) {
return <Tag className={cn('box', className)} {...rest}>{children}</Tag>;
}
// Usage: <Box as="section">, <Box as="a" href="/link">
Twig
{% set tag_name = 'div' %}
{% if url %}
{% set tag_name = 'a' %}
{% set attributes = attributes.setAttribute('href', url) %}
{% endif %}
<{{ tag_name }} {{ attributes.addClass('box') }}>
{% block content %}{% endblock %}
</{{ tag_name }}>
Pattern: Component Composition via Include
React
function StatGroup({ stats }) {
return (
<div className="stats">
{stats.map(stat => (
<StatItem key={stat.id} title={stat.title} value={stat.value} />
))}
</div>
);
}
Twig (caller assembles the composition)
{% embed 'ui_suite_daisyui:stat' %}
{% block items %}
{% include 'ui_suite_daisyui:stat_item' with {
title: 'Downloads', value: '31K', description: '+21% this month'
} %}
{% include 'ui_suite_daisyui:stat_item' with {
title: 'Users', value: '4,200', description: '+14% this month'
} %}
{% endblock %}
{% endembed %}
Pattern: Wrapper Component (HOC Equivalent)
React
function withCard(Component) {
return function CardWrapped(props) {
return (
<div className="card">
<div className="card-body">
<Component {...props} />
</div>
</div>
);
};
}
Twig (using {% embed %})
{% embed 'ui_suite_daisyui:card' with { size: 'lg' } %}
{% block text %}
{% include 'my_theme:custom_content' with { data: data } %}
{% endblock %}
{% endembed %}
{% embed %} is Twig's answer to HOCs and wrapper components. It includes a template and overrides specific blocks.
Common Mistakes
- Wrong: Creating deeply nested SDC includes (5+ levels) → Right: Flatten the hierarchy where possible
- Wrong: Trying to pass a Twig template name as a prop → Right: SDC props do not support component references; use conditional logic or separate component variants
- Wrong: Using
{% include %}when{% embed %}is needed → Right:{% include %}cannot override blocks; use{% embed %}when you need to fill slots from the caller