Messaging

Type-safe message passing between components, bubbling up the tree.

Messages let a deeply nested child component notify an ancestor without threading callbacks through every intermediate component. A message bubbles up from child to parent until it reaches the root or is stopped.

Defining a message

A message is a data class that extends MessageBase:

data class ItemSelected(
    override val sender: Component<*>,
    val itemId: String,
) : MessageBase<Component<*>>(sender)

The sender field tracks which component sent the message. Add any payload fields you need — here it's itemId.

Sending a message

Call sendMessage from any component:

class ItemCard(ctx: Ctx<Props>) : Component<ItemCard.Props>(ctx) {
    data class Props(val item: Item)

    override fun VDom.render() {
        ui.card {
            onClick {
                sendMessage(ItemSelected(sender = this@ItemCard, itemId = props.item.id))
            }
            noui.content {
                noui.header { +props.item.name }
            }
        }
    }
}

The message dispatches on the sender's parent, then continues up the tree. The sender itself does not receive its own message.

Listening for messages

Register a listener in your component's init block with onMessage:

class ItemBrowser(ctx: NoProps) : PureComponent(ctx) {

    private var selectedId: String? by value(null)

    init {
        onMessage<ItemSelected> { msg ->
            selectedId = msg.itemId
        }
    }

    override fun VDom.render() {
        ui.cards {
            items.forEach { item ->
                ItemCard(item)
            }
        }
        selectedId?.let { id ->
            p { +"Selected: $id" }
        }
    }
}

The type parameter on onMessage<ItemSelected> filters by message type — only ItemSelected messages trigger this handler.

Stopping propagation

By default, messages continue bubbling after a handler processes them. To stop a message from reaching further ancestors, call stop():

onMessage<ItemSelected> { msg ->
    selectedId = msg.itemId
    msg.stop()  // no ancestor will see this message
}

When to use messaging

Messaging exists for the cases where callbacks are awkward — three or four levels of nesting, or when a generic container shouldn't need to know about the specific events its children emit. For direct parent-child communication, a callback prop is simpler and more explicit.
Pattern When to use
Callback prop Direct parent-child. The parent passes onSelect: (String) -> Unit as a prop.
Message Deeply nested child needs to notify a distant ancestor. Intermediaries don't need to know.
Stream Cross-cutting state that multiple unrelated components observe. See State Management.