Skip to content

9.6. NiceGUI Integration

NiceGUI is a Python web framework that exposes declarative UI components. Wire a ComponentVMOf[M] by subscribing to the VMx hub and calling element.update() (or rebinding .text, .value, etc.) on property changes.

9.6.1. Reactivity primitive

NiceGUI elements expose mutable .text, .value, .props, and similar attributes. Pushing a new value followed by element.update() re-renders the element. There is no built-in observable model.

9.6.2. Mapping

NiceGUI VMx
ui.label('x').bind_text_from(...) subscribe + assign in handler
ui.button(on_click=fn) command.execute(None) inside on_click
ui.refreshable-decorated builder re-build when CollectionChangedMessage fires
app.add_timer / asyncio loop RxDispatcher.asyncio(loop) → AsyncIOThreadSafeScheduler(loop)

9.6.3. Adapter skeleton

Illustrative, not checked. NiceGUI is not a dependency of this repository, so CI does not compile or run the fences on this page, unlike the recipes listed in How The Recipes Are Checked. Check them against the NiceGUI version your application pins.

Capture the loop inside NiceGUI's running async host callback; do not create an unrelated loop during module import or close the host's loop. The foreground channel is AsyncIOThreadSafeScheduler(loop), while rendering context remains host-owned:

import asyncio

from nicegui import app
from vmx.services import RxDispatcher


@app.on_startup
async def configure_vmx() -> None:
    dispatcher = RxDispatcher.asyncio(asyncio.get_running_loop())
    # Retain dispatcher with the host; dispose and clean up on shutdown.

On shutdown, stop submissions, await admitted hooks while the host loop remains responsive, dispose host-owned VMs and subscriptions, then explicitly clean up the independent pool as shown in Python asyncio dispatcher ownership.

The direct subscription below requires every matching PropertyChangedMessage to arrive on NiceGUI's UI loop and within the current client's rendering context. Background lifecycle terminal messages have that foreground delivery; foreign producers must use a host bridge or foreground observation before they touch an element.

from collections.abc import Callable

from nicegui import ui
import reactivex.operators as ops
from vmx import ComponentVMOf, Message, MessageHubProto, PropertyChangedMessage, RelayCommand

def bind_label(label: ui.label, vm: ComponentVMOf[Note],
               hub: MessageHubProto[Message]) -> Callable[[], None]:
    label.text = vm.model.title

    def _on_msg(msg: PropertyChangedMessage[object]) -> None:
        if msg.property_name == "model":
            label.text = vm.model.title
            label.update()

    sub = hub.messages.pipe(
        ops.filter(lambda m: isinstance(m, PropertyChangedMessage) and m.sender is vm),
    ).subscribe(_on_msg)
    return sub.dispose  # call on page teardown

# Page builder (runs in NiceGUI's host-owned rendering context):
def render(vm: ComponentVMOf[Note], hub: MessageHubProto[Message],
           save_command: RelayCommand) -> None:
    label = ui.label()
    dispose = bind_label(label, vm, hub)
    ui.button("Save", on_click=lambda: save_command.execute(None))
    ui.context.client.on_disconnect(dispose)

9.6.4. Fuller example

No worked NiceGUI Notes-Showcase ships yet. The Textual recipe (textual.md) uses the same hub-subscription shape and is a good reference.