3.1 KiB
Bubble Tea Architecture Best Practices
Model Design
Keep State Flat
❌ Avoid: Deeply nested state ✅ Prefer: Flat structure with clear fields
// Good
type model struct {
items []Item
cursor int
selected map[int]bool
}
// Avoid
type model struct {
state struct {
data struct {
items []Item
}
}
}
Separate Concerns
- UI state in model
- Business logic in separate functions
- Network/IO in commands
Component Ownership
Each component owns its state. Don't reach into component internals.
Update Function
Message Routing
Route messages to appropriate handlers:
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
return m.handleKeyboard(msg)
case tea.WindowSizeMsg:
return m.handleResize(msg)
}
return m.updateComponents(msg)
}
Command Batching
Batch multiple commands:
var cmds []tea.Cmd
cmds = append(cmds, cmd1, cmd2, cmd3)
return m, tea.Batch(cmds...)
View Function
Cache Expensive Renders
Don't recompute on every View() call:
type model struct {
cachedView string
dirty bool
}
func (m model) View() string {
if m.dirty {
m.cachedView = m.render()
m.dirty = false
}
return m.cachedView
}
Responsive Layouts
Adapt to terminal size:
if m.width < 80 {
// Compact layout
} else {
// Full layout
}
Performance
Minimize Allocations
Reuse slices and strings where possible
Defer Heavy Operations
Move slow operations to commands (async)
Debounce Rapid Updates
Don't update on every keystroke for expensive operations
Error Handling
User-Friendly Errors
Show actionable error messages
Graceful Degradation
Fallback when features unavailable
Error Recovery
Allow user to retry or cancel
Testing
Test Pure Functions
Extract business logic for easy testing
Mock Commands
Test Update() without side effects
Snapshot Views
Compare View() output for visual regression
Accessibility
Keyboard-First
All features accessible via keyboard
Clear Indicators
Show current focus, selection state
Help Text
Provide discoverable help (? key)
Code Organization
File Structure
main.go - Entry point, model definition
update.go - Update handlers
view.go - View rendering
commands.go - Command definitions
messages.go - Custom message types
Component Encapsulation
One component per file for complex TUIs
Debugging
Log to File
f, _ := os.OpenFile("debug.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
log.SetOutput(f)
log.Printf("Debug: %+v", msg)
Debug Mode
Toggle debug view with key binding
Common Pitfalls
- Forgetting tea.Batch: Returns only last command
- Not handling WindowSizeMsg: Fixed-size components
- Blocking in Update(): Freezes UI - use commands
- Direct terminal writes: Use tea.Println for above-TUI output
- Ignoring ready state: Rendering before initialization complete