Skip to content

03 - MVU and messages

FlowUI uses a small Model-View-Update loop:

Role Signature Responsibility
Model Any application value Business state
Msg A closed set of message types What happened
Update func(*M, Msg) Cmd[Msg] Apply state changes and effects
View func(*Context, M, Send[Msg]) Widget Render the current state

Message dispatch

Use a Go type switch for a small Update. When the number of message handlers grows, ui.Match lets you split the cases into typed handlers. Both styles are supported; choose based on the size of the update function.

func Update(m *Model, msg Msg) ui.Cmd[Msg] {
    return ui.Match(m, msg,
        ui.On(func(m *Model, _ Increment) ui.Cmd[Msg] {
            m.Count++
            return nil
        }),
        ui.On(func(m *Model, msg NameChanged) ui.Cmd[Msg] {
            m.Name = msg.Name
            return nil
        }),
    )
}

ui.On infers the model, message, and command types from each handler. ui.Match runs the first matching handler and returns nil when none match. Handlers can live in domain-specific functions and be composed, while the application still owns its message types. A type switch remains straightforward when seeing every case in one place is more useful.

Closed message sets

A private marker method keeps the message set explicit and makes type switches easy to review:

type Msg interface{ msg() }

type NameChanged struct{ Value string }
type Submitted struct{}

func (NameChanged) msg() {}
func (Submitted) msg() {}

Update handles messages and returns an optional command:

func Update(m *Model, msg Msg) ui.Cmd[Msg] {
    switch msg := msg.(type) {
    case NameChanged:
        m.Name = msg.Value
    case Submitted:
        return submitCmd(m.Name)
    }
    return nil
}

Single-window programs

Use NewProgram when the initial model is a fixed value:

ui.Run(ui.NewProgram(Model{}, Update, View),
    window.Title("FlowUI app"),
    window.Size(800, 600),
)

Use a full Program value when initialization returns a command or must create fresh reference-backed state:

ui.Run(ui.Program[Model, Msg]{
    Init: func() (Model, ui.Cmd[Msg]) {
        return Model{}, loadInitial()
    },
    Update: Update,
    Subscriptions: func(Model) []ui.Subscription[Msg] { return nil },
    View: View,
})

Nested modules

Child modules can keep their own model and messages. The parent wraps child messages with ui.MapCmd and maps the child's Send callback into a parent message. A complete runnable pattern is maintained in the separate examples module.

Actions and commands

Cmd represents asynchronous effects such as network requests, file IO, and timers. Action and Command describe reusable menu, toolbar, and shortcut actions. They are related but serve different purposes.

Continue with Commands and subscriptions.