This guide covers common issues you might encounter when working with HxComponents and how to resolve them.
- Component Not Rendering
- Form Fields Empty
- Events Not Firing
- Context Errors
- Registration Panics
- HTMX Headers Not Working
- Performance Issues
- Debugging Tips
- Empty response or blank page
- 404 Not Found error
- Component doesn't appear on page
// Ensure component is registered before starting server
registry := components.NewRegistry()
components.Register[*MyComponent](registry, "mycomponent")// Registration name must match URL
components.Register[*MyComponent](registry, "mycomponent")
// URL must be: /component/mycomponent
// NOT: /component/MyComponent (case sensitive!)# Templates must be generated before running
templ generate
# Or watch for changes
templ generate --watch// Component MUST implement templ.Component interface
func (c *MyComponent) Render(ctx context.Context, w io.Writer) error {
return MyTemplate(*c).Render(ctx, w)
}registry := components.NewRegistry()
registry.EnableDebugMode() // Add debugging headers
// Check browser dev tools Network tab for:
// - X-HxComponent-Name
// - X-HxComponent-FormFields
// - X-HxComponent-HasEvent- Struct fields are zero values after form submission
- Form data not being decoded into component
type MyForm struct {
Email string `form:"email"` // Must match <input name="email">
Name string `form:"name"` // Must match <input name="name">
}<!-- HTML input names must match form tags exactly -->
<input name="email" type="email" />
<input name="name" type="text" /><!-- For POST requests, ensure proper content type -->
<form hx-post="/component/myform"
hx-headers='{"Content-Type": "application/x-www-form-urlencoded"}'>
<input name="email" />
</form>// For POST: data comes from request body
// For GET: data comes from query parameters
// Both work, but ensure your HTML uses the right method:
<form hx-post="/component/myform"> <!-- POST -->
<div hx-get="/component/myform?email=test@example.com"> <!-- GET -->import "log/slog"
// Set log level to debug
slog.SetLogLoggerLevel(slog.LevelDebug)
// Check logs for "form decode error" messages// In your Process method, log the received data:
func (c *MyComponent) Process(ctx context.Context) error {
slog.Debug("received form data",
"email", c.Email,
"name", c.Name)
return nil
}- Event handler method not being called
- No errors in logs
- Button clicks don't do anything
// Event name in HTML: "increment"
// Method name MUST be: "OnIncrement" (capitalize first letter)
func (c *CounterComponent) OnIncrement() error {
c.Count++
return nil
}<!-- Event name must match method (case-insensitive) -->
<button hx-vals='{"hxc-event": "increment"}'>+</button><!-- MUST include hxc-event in hx-vals -->
<button hx-post="/component/counter"
hx-vals='{"count": 5, "hxc-event": "increment"}'>
+
</button>
<!-- Or as hidden field -->
<form hx-post="/component/counter">
<input type="hidden" name="hxc-event" value="increment" />
<button type="submit">Submit</button>
</form>// Correct signature:
func (c *Component) OnMyEvent() error {
return nil
}
// WRONG - no parameters allowed (except receiver):
func (c *Component) OnMyEvent(ctx context.Context) error { // ❌
return nil
}// Correct - starts with capital letter:
func (c *Component) OnIncrement() error { // ✅
// Wrong - lowercase means unexported:
func (c *Component) onIncrement() error { // ❌registry.EnableDebugMode()
// Check response headers:
// X-HxComponent-HasEvent: true/false- "context deadline exceeded" error
- "context canceled" error
- Database timeouts
func (c *Component) BeforeEvent(ctx context.Context, eventName string) error {
// Add timeout to context
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
items, err := db.GetItems(ctx, c.UserID)
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
return fmt.Errorf("database query timeout")
}
return err
}
c.Items = items
return nil
}func (c *Component) Process(ctx context.Context) error {
for i := 0; i < 1000; i++ {
// Check if context was cancelled
select {
case <-ctx.Done():
return ctx.Err()
default:
// Continue processing
}
// Do work...
}
return nil
}// WRONG - don't create background context:
func (c *Component) Process(ctx context.Context) error {
newCtx := context.Background() // ❌ Loses cancellation
db.Query(newCtx, "...")
return nil
}
// Correct - use provided context:
func (c *Component) Process(ctx context.Context) error {
db.Query(ctx, "...") // ✅
return nil
}- Panic on startup: "component already registered"
- Panic: "component type must be a pointer type"
- Panic: "component must point to a struct"
// WRONG - duplicate registration:
components.Register[*MyComponent](registry, "mycomponent")
components.Register[*MyComponent](registry, "mycomponent") // ❌ Panic!
// Correct - register once:
components.Register[*MyComponent](registry, "mycomponent")// WRONG - not a pointer:
components.Register[MyComponent](registry, "mycomponent") // ❌
// Correct - must be pointer:
components.Register[*MyComponent](registry, "mycomponent") // ✅// WRONG - can't register primitive types:
components.Register[*string](registry, "test") // ❌
// Correct - must be a struct:
type MyComponent struct {
Data string
}
components.Register[*MyComponent](registry, "test") // ✅- HX-Redirect not redirecting
- HX-Trigger not firing events
- Request headers not being captured
type LoginComponent struct {
RedirectTo string `json:"-"`
}
// Must implement interface to set header:
func (c *LoginComponent) GetHxRedirect() string {
return c.RedirectTo
}type MyComponent struct {
IsBoosted bool `json:"-"`
}
// Must implement setter interface:
func (c *MyComponent) SetHxBoosted(v bool) {
c.IsBoosted = v
}func (c *LoginComponent) Process(ctx context.Context) error {
if c.Username == "demo" && c.Password == "password" {
c.RedirectTo = "/dashboard" // This will set HX-Redirect header
return nil
}
return nil
}// Response headers must have exact getter names:
GetHxRedirect() // Sets: HX-Redirect
GetHxTrigger() // Sets: HX-Trigger
GetHxPushUrl() // Sets: HX-Push-Url
// Request headers must have exact setter names:
SetHxBoosted(bool) // Reads: HX-Boosted
SetHxRequest(bool) // Reads: HX-Request
SetHxCurrentURL(string) // Reads: HX-Current-URL- Slow component rendering
- High memory usage
- Database connection pool exhaustion
func (c *Component) BeforeEvent(ctx context.Context, eventName string) error {
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
// Query with timeout
data, err := db.Query(ctx, "...")
return err
}// BAD - loading all data:
func (c *Component) BeforeEvent(ctx context.Context, eventName string) error {
c.Items = db.GetAllItems(ctx) // Could be millions of rows!
return nil
}
// GOOD - pagination:
func (c *Component) BeforeEvent(ctx context.Context, eventName string) error {
c.Items = db.GetItems(ctx, c.Page, 20) // Only 20 items
return nil
}3. Use Hidden Fields for Small State
// Instead of reloading from DB every time:
type Component struct {
Count int `form:"count"` // Passed via hidden field
}<input type="hidden" name="count" value="{ fmt.Sprint(data.Count) }" />func (c *Component) BeforeEvent(ctx context.Context, eventName string) error {
// Check cache first
if cached, ok := cache.Get(c.UserID); ok {
c.Data = cached
return nil
}
// Load from database
data, err := db.LoadData(ctx, c.UserID)
if err != nil {
return err
}
// Store in cache
cache.Set(c.UserID, data, 5*time.Minute)
c.Data = data
return nil
}registry := components.NewRegistry()
registry.EnableDebugMode()
// Check browser Network tab for debug headers:
// - X-HxComponent-Name: component name
// - X-HxComponent-FormFields: number of fields
// - X-HxComponent-HasEvent: true/falseimport "log/slog"
// Enable debug level logging
slog.SetLogLoggerLevel(slog.LevelDebug)
// Or use JSON handler for structured logging:
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
})
slog.SetDefault(slog.New(handler))- Open browser DevTools (F12)
- Go to Network tab
- Trigger your component
- Click on the request
- Check:
- Request Headers (HX-*)
- Form Data
- Response Headers (HX-*)
- Response Preview
func TestMyComponent(t *testing.T) {
// Full browser testing
page := setupPlaywright(t)
page.Goto("http://localhost:8080")
// Interact and inspect
page.Click("#my-button")
content := page.Locator("#result").TextContent()
require.Contains(t, content, "Expected text")
}// Add strategic logging in your components:
func (c *Component) Process(ctx context.Context) error {
slog.Info("processing component",
"user_id", c.UserID,
"action", c.Action)
// Your logic here
slog.Info("processing complete",
"success", true)
return nil
}Cause: Component name in URL doesn't match registered name Fix: Check that URL path matches the registered name exactly (case-sensitive)
Cause: Using wrong HTTP method (e.g., PUT, DELETE) Fix: Only GET and POST are supported
Cause: Form tag doesn't match HTML input name, or invalid data type Fix: Ensure form tags match input names exactly
Cause: Missing Render method or wrong signature
Fix: Implement Render(ctx context.Context, w io.Writer) error
Cause: Method name doesn't match event name
Fix: Ensure method is named On + capitalized event name
Cause: Error returned from lifecycle hook Fix: Check your hook implementation for bugs
If you're still stuck:
- Check the Examples - Look at
/examplesdirectory for working code - Read the Docs - See docs/README.md for comprehensive guides
- Enable Debug Mode - Use
registry.EnableDebugMode()and check headers - Check Logs - Enable debug logging:
slog.SetLogLoggerLevel(slog.LevelDebug) - GitHub Issues - Search or create an issue at https://github.com/ocomsoft/HxComponents/issues
- ✅ Always run
templ generatebefore testing - ✅ Use debug mode during development
- ✅ Enable debug logging to see what's happening
- ✅ Test with browser DevTools Network tab open
- ✅ Use exact naming for form tags and input names
- ✅ Implement Render method correctly
- ✅ Use pointer types for component registration
- ✅ Check context errors in database queries
- ✅ Respect context cancellation in long-running operations
- ✅ Write tests - unit, integration, and E2E
Last Updated: 2025-10-23