State Reference
Overview
State[T] and Events[T] are the two generic types that handle reactivity in go-tui. State[T] holds a value and triggers re-renders when it changes. Events[T] broadcasts messages between components without shared mutable state.
Both types follow the same lifecycle pattern: create them unbound, and the framework binds them to an App during component mounting. Once bound, any mutation marks the UI dirty so the next frame picks up the change.
// In a struct component
type counter struct {
count *tui.State[int]
}
func Counter() *counter {
return &counter{
count: tui.NewState(0),
}
}
State[T]
A reactive container for a single value of type T. When the value changes via Set or Update, the framework marks the app dirty (triggering a re-render) and notifies any registered bindings.
NewState
func NewState[T any](initial T) *State[T]
Creates a new state with the given initial value. The state starts unbound and the framework calls BindApp automatically when the owning component mounts.
Works with any type: primitives, slices, structs, maps.
count := tui.NewState(0) // State[int]
name := tui.NewState("hello") // State[string]
items := tui.NewState([]string{}) // State[[]string]
visible := tui.NewState(true) // State[bool]
NewStateForApp
func NewStateForApp[T any](app *App, initial T) *State[T]
Creates a state already bound to the given app. Use this when creating state outside of a component (for example, in main.go before passing it to a component constructor). Panics if app is nil.
app, _ := tui.NewApp(...)
data := tui.NewStateForApp(app, []string{"a", "b", "c"})
BindApp
func (s *State[T]) BindApp(app *App)
Binds the state to an app for dirty-marking and batching. The framework calls this during component mounting; you rarely need to call it yourself. Panics if app is nil. Idempotent if called with the same app; overwrites if called with a different one.
Get
func (s *State[T]) Get() T
Returns the current value. Safe to call from any goroutine.
current := count.Get()
In .gsx render methods, call Get() inside expressions:
<span>{fmt.Sprintf("Count: %d", s.count.Get())}</span>
Set
func (s *State[T]) Set(v T)
Replaces the value, marks the app dirty, and notifies all bindings. If called within a Batch, binding execution is deferred until the batch completes.
Must be called from the main event loop. For updates from background goroutines, use app.QueueUpdate or channel watchers.
count.Set(42)
Circular dependencies: If a binding callback on state A triggers a Set on state A (directly, or through a chain like A→B→A), the value is updated and dirty is marked, but bindings are not fired again. Without this guard, the recursive calls would overflow the stack. Render() reads the correct final values on the next frame.
Non-circular dependencies are unaffected. If A's binding sets B and C's binding also sets B, B's bindings fire once for each call. The guard only activates when Set is called on a state that is already in the middle of executing its own bindings.
Update
func (s *State[T]) Update(fn func(T) T)
Reads the current value, applies fn, and sets the result. A convenience for read-modify-write operations.
count.Update(func(v int) int { return v + 1 })
items.Update(func(list []string) []string {
return append(list, "new item")
})
Bind
func (s *State[T]) Bind(fn func(T)) Unbind
Registers a callback that fires whenever the value changes via Set or Update. The callback receives the new value. Bindings execute in registration order.
Returns an Unbind function. Call it to remove the binding and prevent future invocations.
unbind := count.Bind(func(v int) {
fmt.Println("count changed to", v)
})
// Later, to stop receiving updates:
unbind()
Unbind
type Unbind func()
A handle returned by Bind. Calling it deactivates the associated binding. Safe to call multiple times.
Events[T]
An event bus for broadcasting messages between components. Unlike State[T], Events[T] does not store a value. It delivers each emitted event to all current subscribers and marks the app dirty.
NewEvents
func NewEvents[T any](topic string) *Events[T]
Creates a new topic-based event bus. topic is required and is used to route events across components.
notifications := tui.NewEvents[string]("app.notifications")
NewEventsForApp
func NewEventsForApp[T any](app *App, topic string) *Events[T]
Creates an event bus already bound to the given app. Use this when creating a bus outside of a component. Panics if app is nil.
app, _ := tui.NewApp(...)
bus := tui.NewEventsForApp[string](app, "app.notifications")
BindApp
func (e *Events[T]) BindApp(app *App)
Binds the event bus to an app for dirty-marking. The framework calls this during component mounting. Panics if app is nil. Idempotent for the same app; overwrites if called with a different one.
UnbindApp
func (e *Events[T]) UnbindApp()
Detaches the event bus from app topic routing. Called automatically when components unmount or when the root is replaced.
Emit
func (e *Events[T]) Emit(event T)
Sends an event to all subscribers and marks the app dirty. Subscribers are called synchronously in registration order.
Panics if the event bus has no bound app.
notifications.Emit("task completed")
Subscribe
func (e *Events[T]) Subscribe(fn func(T)) func()
Registers a handler that will be called for every emitted event. Handlers run on the UI thread, so it is safe to update state from within a handler.
unsub := notifications.Subscribe(func(msg string) {
log.Set(append(log.Get(), msg))
})
defer unsub()
Batching
App.Batch
func (a *App) Batch(fn func())
Executes fn with all state binding callbacks deferred. Within a batch, each Set or Update call still marks the app dirty immediately, but binding callbacks accumulate and run once when the outermost Batch returns. If the same binding is triggered multiple times during a batch, only the last value is delivered.
Batches can nest. Deferred callbacks only fire when the outermost batch completes.
app.Batch(func() {
firstName.Set("Alice")
lastName.Set("Smith")
age.Set(30)
}) // All binding callbacks fire here, once
Use Batch when you need to update several state values at once and want to avoid intermediate binding work.
Thread Safety
State[T] and Events[T] follow the same threading rules as the rest of go-tui:
Get()is safe from any goroutine. Read the value whenever you need it.Set(),Update(), andEmit()must run on the main event loop. The event loop is the single thread that processes input and renders frames.- Watcher callbacks run on the event loop. Handlers passed to
tui.OnTimerandtui.Watchare safe to callSetfrom. - For background goroutines, use
app.QueueUpdate. This enqueues a function on the event loop whereSetis safe.
// Background goroutine updating state safely
go func() {
result := fetchData()
app.QueueUpdate(func() {
data.Set(result) // runs on the event loop
})
}()
Getting *tui.App into a component
For samplers, pollers, or any goroutine spawned from a component method, the component needs an *tui.App reference to call QueueUpdate. Declare a field of that type on the struct; the generator's BindApp assigns it automatically along with delegating to every State and Events field:
type MyComponent struct {
app *tui.App // auto-assigned on mount
count *tui.State[int]
}
templ (c *MyComponent) Render() { <span>{count}</span> }
The generator also emits an unexported bindAppFields(app *tui.App) method on the receiver. If you override BindApp (for custom setup), call bindAppFields from inside your override so the State and Events delegations don't have to be maintained by hand:
func (c *MyComponent) BindApp(app *tui.App) {
c.bindAppFields(app)
// custom logic here
}
A user-defined BindApp that skips bindAppFields will leave State fields unbound, causing Set to panic or silently drop updates.
Practical Patterns
Derived display values
Use Go expressions directly in .gsx to compute display strings from state:
templ (c *counter) Render() {
<span class="font-bold">{fmt.Sprintf("Count: %d", c.count.Get())}</span>
}
Reusable element fragments with :=
The := binding assigns an element to a name so you can reuse it in multiple places. It requires an element (starting with <), not a Go expression.
templ (s *stateApp) Render() {
countBadge := <span class="text-cyan font-bold">{fmt.Sprintf("%d", s.count.Get())}</span>
<div class="flex-col gap-1">
{countBadge}
</div>
}
Conditional rendering from state
templ (a *myApp) Render() {
if a.loading.Get() {
<span class="text-yellow">Loading...</span>
} else {
<span class="text-green">Ready</span>
}
}
Shared state between components
Create state in a parent and pass it to child constructors. Both components react to changes from either side.
type app struct {
selected *tui.State[int]
sidebar *sidebar
content *contentPanel
}
func App() *app {
sel := tui.NewState(0)
return &app{
selected: sel,
sidebar: Sidebar(sel),
content: ContentPanel(sel),
}
}
Cross-component events
Use Events[T] when components need to communicate without sharing mutable state:
type app struct {
header *header
body *body
}
func App() *app {
return &app{
header: Header(),
body: Body(),
}
}
Both components can construct tui.NewEvents[string]("app.alerts") internally and communicate through that shared topic without passing bus pointers.