Getting Started

The Elm Architecture

Understanding the Model-Update-View pattern that powers Bubble Tea applications.

Bubble Tea is based on the functional design paradigms of The Elm Architecture, which happens to work nicely with Go. It's a delightful way to build applications.

The Three Core Components

Bubble Tea programs are comprised of a model that describes the application state and three simple methods on that model:

Init

A function that returns an initial command for the application to run. This is where you set up your initial state.

func (m model) Init() (tea.Model, tea.Cmd) {
    // Set up initial state
    return m, nil
}

Update

A function that handles incoming events and updates the model accordingly. Think of this as your event handler.

func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg := msg.(type) {
    case tea.KeyPressMsg:
        switch msg.String() {
        case "q", "ctrl+c":
            return m, tea.Quit
        }
    }
    return m, nil
}

View

A function that renders the UI based on the data in the model. It returns a string that represents your entire UI.

func (m model) View() string {
    return "Hello, Bubble Tea!"
}

The Model

The model describes your application's state. It can be any type, but a struct usually makes the most sense:

type model struct {
    choices  []string         // items on the to-do list
    cursor   int              // which item our cursor is pointing at
    selected map[int]struct{} // which items are selected
}

Messages

Messages are events. They can be keypresses, mouse events, timer ticks, or anything else you define:

switch msg := msg.(type) {
case tea.KeyPressMsg:
    // Handle key press
case tea.MouseMsg:
    // Handle mouse event
case tickMsg:
    // Handle custom tick message
}

Commands

Commands are functions that perform I/O and return a message. They're how you handle side effects:

func checkServer() tea.Msg {
    res, err := http.Get("https://api.example.com")
    if err != nil {
        return errMsg{err}
    }
    return statusMsg(res.StatusCode)
}

// Use it in Update:
return m, checkServer
Commands run asynchronously and their results come back as messages to your Update function.

Putting It Together

Here's the flow:

graph LR
    A[Init] --> B[View]
    B --> C[User Input]
    C --> D[Update]
    D --> B
    D --> E[Commands]
    E --> D
  1. Init sets up your initial model and optional command
  2. View renders based on the current model
  3. User input or command results trigger Update
  4. Update returns a new model and optional commands
  5. The cycle continues
The key insight is that your UI is always a pure function of your model. No hidden state, no surprises.