This guide covers patterns for composing and combining HxComponents to build complex UIs.
Components can render other components directly in their templates.
parent.go:
package parent
import (
"context"
"io"
)
type ParentComponent struct {
ParentData string
}
func (p *ParentComponent) Render(ctx context.Context, w io.Writer) error {
return Parent(*p).Render(ctx, w)
}parent.templ:
package parent
import "myproject/components/child"
templ Parent(data ParentComponent) {
<div class="parent">
<h1>Parent Component</h1>
<p>Parent data: { data.ParentData }</p>
<!-- Render child component directly -->
@child.Child(child.ChildComponent{
Message: data.ParentData,
})
</div>
}child.go:
package child
import (
"context"
"io"
)
type ChildComponent struct {
Message string
}
func (c *ChildComponent) Render(ctx context.Context, w io.Writer) error {
return Child(*c).Render(ctx, w)
}child.templ:
package child
templ Child(data ChildComponent) {
<div class="child">
<p>Child received: { data.Message }</p>
</div>
}Each component can manage its own state and handle its own events independently.
// dashboard.templ
package dashboard
import (
"myproject/components/counter"
"myproject/components/todolist"
"myproject/components/chart"
)
templ Dashboard(data DashboardComponent) {
<div class="dashboard">
<div class="sidebar">
<h2>Counter</h2>
<!-- Counter handles its own increment/decrement events -->
@counter.Counter(counter.CounterComponent{
Count: data.CounterValue,
})
</div>
<div class="main">
<h2>Tasks</h2>
<!-- TodoList handles its own add/delete/toggle events -->
@todolist.TodoList(todolist.TodoListComponent{
Title: "My Tasks",
Items: data.Tasks,
})
</div>
<div class="stats">
<h2>Statistics</h2>
<!-- Chart renders data visualization -->
@chart.Chart(chart.ChartComponent{
Data: data.ChartData,
})
</div>
</div>
}Pass dynamic content to child components using templ.Component.
package card
import (
"context"
"io"
"github.com/a-h/templ"
)
type CardComponent struct {
HeaderContent templ.Component
BodyContent templ.Component
FooterContent templ.Component
}
func (c *CardComponent) Render(ctx context.Context, w io.Writer) error {
return Card(*c).Render(ctx, w)
}package card
templ Card(data CardComponent) {
<div class="card">
if data.HeaderContent != nil {
<div class="card-header">
@data.HeaderContent
</div>
}
<div class="card-body">
@data.BodyContent
</div>
if data.FooterContent != nil {
<div class="card-footer">
@data.FooterContent
</div>
}
</div>
}@card.Card(card.CardComponent{
HeaderContent: templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
_, err := io.WriteString(w, "<h2>Card Title</h2>")
return err
}),
BodyContent: templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
_, err := io.WriteString(w, "<p>Card body content goes here</p>")
return err
}),
FooterContent: templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
_, err := io.WriteString(w, "<button>Action</button>")
return err
}),
})// Helper to create templ.Component from HTML string
func HTML(html string) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
_, err := io.WriteString(w, html)
return err
})
}
// Usage
@card.Card(card.CardComponent{
HeaderContent: HTML("<h2>Title</h2>"),
BodyContent: HTML("<p>Content</p>"),
})package shared
type SharedState struct {
UserID int
Theme string
CartItems int
}
func GetSharedState(ctx context.Context) *SharedState {
session := getSessionFromContext(ctx)
return &SharedState{
UserID: session.GetInt("userID"),
Theme: session.GetString("theme"),
CartItems: session.GetInt("cartItems"),
}
}
func SaveSharedState(ctx context.Context, state *SharedState) error {
session := getSessionFromContext(ctx)
session.Set("userID", state.UserID)
session.Set("theme", state.Theme)
session.Set("cartItems", state.CartItems)
return session.Save()
}// In component
type HeaderComponent struct {
ctx context.Context `json:"-"`
UserID int
Theme string
CartItems int
}
func (c *HeaderComponent) BeforeEvent(ctx context.Context, eventName string) error {
state := shared.GetSharedState(ctx)
c.UserID = state.UserID
c.Theme = state.Theme
c.CartItems = state.CartItems
return nil
}package services
type UserService struct {
db *sql.DB
}
func (s *UserService) GetUser(id int) (*User, error) {
// Load from database
return user, nil
}
func (s *UserService) UpdatePreferences(id int, prefs *Preferences) error {
// Save to database
return nil
}
// Global service instance (or use dependency injection)
var Users = &UserService{db: db}// In component
type ProfileComponent struct {
UserID int `form:"userId"`
User *User `json:"-"`
}
func (c *ProfileComponent) BeforeEvent(ctx context.Context, eventName string) error {
user, err := services.Users.GetUserWithContext(ctx, c.UserID)
if err != nil {
return err
}
c.User = user
return nil
}// Middleware to inject user into context
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Get user from session/token
user := getUserFromRequest(r)
// Add to context
ctx := context.WithValue(r.Context(), "user", user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// In component
func (c *Component) BeforeEvent(ctx context.Context, eventName string) error {
user := ctx.Value("user").(*User)
c.CurrentUser = user
return nil
}One component triggers an event, another listens for it.
Action Component:
func (c *ActionComponent) OnDoAction() error {
// Perform action
c.ActionCompleted = true
// Trigger HTMX event
c.GetHTMXResponseHeaders()["HX-Trigger"] = "actionCompleted"
return nil
}<button
hx-post="/component/action"
hx-vals='{"hxc-event": "doAction"}'
>
Do Action
</button>Listener Component:
<div
hx-get="/component/listener"
hx-trigger="actionCompleted from:body"
hx-swap="innerHTML"
>
Waiting for action...
</div>Update multiple components from a single request.
templ ShoppingCart(data CartComponent) {
<!-- Main cart component -->
<div id="cart">
<h2>Shopping Cart</h2>
for _, item := range data.Items {
<div>{ item.Name } - ${ fmt.Sprintf("%.2f", item.Price) }</div>
}
<p>Total: ${ fmt.Sprintf("%.2f", data.Total) }</p>
</div>
<!-- Out-of-band update for header cart count -->
<div id="cart-count" hx-swap-oob="true">
{ fmt.Sprint(len(data.Items)) }
</div>
<!-- Out-of-band update for cart total in footer -->
<div id="cart-total-footer" hx-swap-oob="true">
Total: ${ fmt.Sprintf("%.2f", data.Total) }
</div>
}Broadcast changes to all listening components.
type EventBus struct {
subscribers map[string][]chan Event
mu sync.RWMutex
}
func (e *EventBus) Publish(eventType string, data interface{}) {
e.mu.RLock()
defer e.mu.RUnlock()
if subs, ok := e.subscribers[eventType]; ok {
for _, ch := range subs {
ch <- Event{Type: eventType, Data: data}
}
}
}
// Component publishes event
func (c *Component) OnUpdate() error {
// Update data
c.UpdateData()
// Publish to event bus
eventBus.Publish("dataUpdated", c.Data)
return nil
}Create reusable layout components.
package layouts
type PageLayoutComponent struct {
Title string
Content templ.Component
User *User
}package layouts
templ PageLayout(data PageLayoutComponent) {
<!DOCTYPE html>
<html>
<head>
<title>{ data.Title }</title>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<link rel="stylesheet" href="/static/css/app.css" />
</head>
<body>
<nav>
<a href="/">Home</a>
if data.User != nil {
<span>Welcome, { data.User.Name }</span>
<a href="/logout">Logout</a>
} else {
<a href="/login">Login</a>
}
</nav>
<main>
@data.Content
</main>
<footer>
<p>© 2024 My App</p>
</footer>
</body>
</html>
}package pages
import "myproject/layouts"
import "myproject/components/dashboard"
templ DashboardPage(user *User, dashData dashboard.DashboardComponent) {
@layouts.PageLayout(layouts.PageLayoutComponent{
Title: "Dashboard",
User: user,
Content: templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
return dashboard.Dashboard(dashData).Render(ctx, w)
}),
})
}Create components dynamically based on type.
package components
type WidgetType string
const (
WidgetTypeChart WidgetType = "chart"
WidgetTypeTable WidgetType = "table"
WidgetTypeCounter WidgetType = "counter"
)
type Widget struct {
Type WidgetType
Data interface{}
}
func CreateWidget(widget Widget) templ.Component {
switch widget.Type {
case WidgetTypeChart:
data := widget.Data.(chart.ChartData)
return chart.Chart(chart.ChartComponent{Data: data})
case WidgetTypeTable:
data := widget.Data.(table.TableData)
return table.Table(table.TableComponent{Data: data})
case WidgetTypeCounter:
data := widget.Data.(counter.CounterData)
return counter.Counter(counter.CounterComponent{Count: data.Count})
default:
return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
return fmt.Errorf("unknown widget type: %s", widget.Type)
})
}
}templ Dashboard(data DashboardComponent) {
<div class="dashboard">
for _, widget := range data.Widgets {
<div class="widget">
@CreateWidget(widget)
</div>
}
</div>
}Wrap components with additional functionality.
// WithAuth wraps a component and checks authentication
func WithAuth(comp templ.Component) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
user := ctx.Value("user").(*User)
if user == nil {
return HTML("<div>Please log in</div>").Render(ctx, w)
}
return comp.Render(ctx, w)
})
}
// WithLoading wraps a component with loading indicator
func WithLoading(comp templ.Component, loading bool) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
if loading {
return HTML(`<div class="spinner">Loading...</div>`).Render(ctx, w)
}
return comp.Render(ctx, w)
})
}// Usage
@WithAuth(dashboard.Dashboard(data))
@WithLoading(chart.Chart(chartData), isLoading)Register components dynamically for plugin-like architecture.
type ComponentRegistry struct {
components map[string]func(data interface{}) templ.Component
}
func NewComponentRegistry() *ComponentRegistry {
return &ComponentRegistry{
components: make(map[string]func(data interface{}) templ.Component),
}
}
func (r *ComponentRegistry) Register(name string, factory func(data interface{}) templ.Component) {
r.components[name] = factory
}
func (r *ComponentRegistry) Get(name string, data interface{}) templ.Component {
if factory, ok := r.components[name]; ok {
return factory(data)
}
return HTML(fmt.Sprintf("<div>Component '%s' not found</div>", name))
}
// Global registry
var Components = NewComponentRegistry()
// Register components at startup
func init() {
Components.Register("counter", func(data interface{}) templ.Component {
return counter.Counter(data.(counter.CounterComponent))
})
Components.Register("chart", func(data interface{}) templ.Component {
return chart.Chart(data.(chart.ChartComponent))
})
}// Dynamic component rendering
@Components.Get(widgetType, widgetData)-
Keep Components Focused
- Each component should have a single responsibility
- Split large components into smaller, reusable pieces
-
Use Composition Over Inheritance
- Compose complex UIs from simple components
- Pass components as props for flexibility
-
Manage State at the Right Level
- Local state: Component fields
- Shared state: Session or database
- Global state: Context or service layer
-
Document Component Interfaces
- Document expected props/data
- Document events and behavior
- Provide usage examples
-
Test Components in Isolation
- Unit test component logic
- Integration test component interactions
- E2E test critical user flows
-
Consider Performance
- Lazy load heavy components
- Cache expensive computations
- Use efficient data structures
-
Handle Errors Gracefully
- Validate props/data
- Return meaningful error messages
- Provide fallback UI for errors
-
Make Components Reusable
- Parameterize behavior
- Avoid hard-coding values
- Support customization through props