State and Binding

State is the foundation of the declarative API. This chapter covers State, Computed, and two-way binding.

Basic state

State[T] holds a value and notifies subscribers when it changes:

"""State basics — create, read, modify, subscribe."""

import libui
from libui.declarative import App, Window, VBox, Label, Button, Entry, State


async def main():
    app = App()

    name = State("World")
    greeting = name.map(lambda n: f"Hello, {n}!")

    # Subscribe to changes (prints to console)
    name.subscribe(lambda: print(f"Name changed to: {name.value}"))

    app.build(
        window=Window(
            "State Binding",
            400,
            300,
            child=VBox(
                Label(text=greeting),
                Entry(text=name),
                Button("Reset", on_clicked=lambda: name.set("World")),
            ),
        )
    )

    app.show()
    await app.wait()


libui.run(main())
State binding demo

Key points:

  • State("World") — creates a state with an initial value

  • name.value = "Python" — setting .value notifies all subscribers

  • name.set("value") — equivalent to setting .value

  • name.subscribe(cb) — registers a callback; returns an unsubscribe function

Computed state

Computed is a read-only derived state created with .map():

"""Computed state — derived read-only values that auto-update."""

import libui
from libui.declarative import App, Window, VBox, Form, Label, Button, State


async def main():
    app = App()

    count = State(0)
    doubled = count.map(lambda n: n * 2)
    label_text = count.map(lambda n: f"Count: {n}")
    doubled_text = doubled.map(lambda n: f"Doubled: {n}")
    parity = count.map(lambda n: "even" if n % 2 == 0 else "odd")
    parity_text = parity.map(lambda p: f"Parity: {p}")

    app.build(
        window=Window(
            "Computed State",
            400,
            250,
            child=VBox(
                Form(
                    ("Count:", Label(text=label_text)),
                    ("Doubled:", Label(text=doubled_text)),
                    ("Parity:", Label(text=parity_text)),
                ),
                Button("Increment", on_clicked=lambda: count.update(lambda n: n + 1)),
                Button("Reset", on_clicked=lambda: count.set(0)),
            ),
        )
    )

    app.show()
    await app.wait()


libui.run(main())
Computed state demo

Key points:

  • count.map(fn) — creates a Computed that auto-updates when count changes

  • Computed values can be chained: a.map(f).map(g)

  • Computed values are read-only — you can’t set them directly

  • Pass Computed to widget props like Label(text=...) for automatic updates

Two-way binding

When you pass a State to a widget that supports user input, you get two-way binding — the widget updates the state, and state changes update the widget:

text = State("")
Entry(text=text)  # two-way: typing updates state, state updates entry

Compare with one-way binding:

Label(text=text)           # one-way: state -> widget (State)
Label(text=text.map(str))  # one-way: state -> widget (Computed)
Label(text="static")       # no binding: plain value

Widgets that support two-way binding: Entry, MultilineEntry, Checkbox, Slider, Spinbox, Combobox, EditableCombobox, RadioButtons.