Skip to main content

Communicating Between Components

This page covers patterns for communicating between Litho components, including parent-child, sibling, and external communication.

Overview​

Litho components communicate through several patterns:

PatternDirectionUse Case
Callback propsChild → ParentNotify parent of events
Props updatesParent → ChildPass new data to children
ControllersParent → ChildTrigger imperative actions
Parent mediationSibling ↔ SiblingCoordinate between siblings
StateFlow/ListenersExternal → ComponentReact to external changes
Observer callbacksComponent → ExternalNotify external systems
Thread Safety

A Component's render method may run on a background thread. It's important that any communication mechanisms do not perform side-effects from render. Side-effects can instead be performed from main thread events like onClick and useEffect.

Dispatching events from child to parent​

Sometimes a parent needs to know when an event happens in a child component. For example, a ProductCard needs to know when its AddToCartButton is clicked. The parent passes a lambda callback to the child:

class ProductCard : KComponent() {
override fun ComponentScope.render(): Component {
val cartItemCount = useState { 0 }

return Column {
child(Text(text = "Product: Wireless Headphones"))
child(Text(text = "Price: $99.99"))
child(Text(text = "Items in cart: ${cartItemCount.value}"))
child(AddToCartButton(onAddToCart = { cartItemCount.update { it + 1 } }))
}
}
}

The child component invokes the callback to notify the parent that the action occurred:

class AddToCartButton(private val onAddToCart: () -> Unit) : KComponent() {
override fun ComponentScope.render(): Component {
return Text(text = "Add to Cart", style = Style.onClick { onAddToCart() })
}
}

Passing new data from parent to child​

When a parent component needs to update a child, it passes new props. For example, a CartScreen updates a CartTotal component when items are added. The parent triggers a state update, and the child receives the latest value through its props:

class CartScreen : KComponent() {
override fun ComponentScope.render(): Component {
val totalPrice = useState { 0.0 }

return Column {
child(Text(text = "Your Shopping Cart"))
// When clicked, add an item and update the total
child(
Text(
text = "Add $29.99 item",
style = Style.onClick { totalPrice.update { it + 29.99 } },
),
)
// CartTotal receives the current total as a prop
child(CartTotal(total = totalPrice.value))
}
}
}

class CartTotal(private val total: Double) : KComponent() {
override fun ComponentScope.render(): Component {
return Text(text = "Total: $${String.format("%.2f", total)}")
}
}

Triggering an action on a child from a parent​

Sometimes a parent needs to trigger an imperative action on a child rather than just passing new props. For example, a CheckoutForm might need to trigger validation on a PaymentCardInput. Use the Controllers Pattern for this:

class PaymentInputController {
private var validationListener: (() -> Unit)? = null

fun setValidationListener(listener: () -> Unit) {
validationListener = listener
}

fun clearValidationListener() {
validationListener = null
}

fun validate() {
validationListener?.invoke()
}
}

The parent creates the controller and triggers validation when the user submits:

class CheckoutForm : KComponent() {
override fun ComponentScope.render(): Component {
val controller = useCached { PaymentInputController() }

return Column {
child(PaymentCardInput(controller = controller))
child(Text(text = "Place Order", style = Style.onClick { controller.validate() }))
}
}
}

The child registers a listener with the controller:

class PaymentCardInput(private val controller: PaymentInputController) : KComponent() {
override fun ComponentScope.render(): Component {
val isValidated = useState { false }

useEffect(controller) {
controller.setValidationListener { isValidated.update { true } }
onCleanup { controller.clearValidationListener() }
}

return Text(text = if (isValidated.value) "Card validated ✓" else "Enter card details...")
}
}

Communicating between siblings​

Two sibling components cannot communicate directly. Instead, use a parent as mediator pattern:

  1. Child A dispatches an event to the parent
  2. Parent updates state or triggers an action
  3. Child B receives the update through props or a controller

This ensures a unidirectional data flow that's easier to debug and maintain.

For example, a PromoCodeInput applies a discount that updates an OrderSummary. The PromoCodeInput sends the discount to the parent:

class PromoCodeInput(private val onPromoApplied: (discountPercent: Int) -> Unit) : KComponent() {
override fun ComponentScope.render(): Component {
return Text(
text = "Apply 20% discount",
style = Style.onClick { onPromoApplied(20) },
)
}
}

The OrderSummary receives the discount as a prop and displays the updated total:

class OrderSummary(private val discountPercent: Int) : KComponent() {
override fun ComponentScope.render(): Component {
val subtotal = 99.99
val discount = subtotal * discountPercent / 100
val total = subtotal - discount

return Column {
child(Text(text = "Subtotal: $${String.format("%.2f", subtotal)}"))
if (discountPercent > 0) {
child(Text(text = "Discount: -$${String.format("%.2f", discount)} ($discountPercent%)"))
}
child(Text(text = "Total: $${String.format("%.2f", total)}"))
}
}
}

The CheckoutScreen mediates between the siblings by holding the shared state:

class CheckoutScreen : KComponent() {
override fun ComponentScope.render(): Component {
val discountPercent = useState { 0 }

return Column {
// PromoCodeInput sends discount via callback
child(PromoCodeInput(onPromoApplied = { percent -> discountPercent.update { percent } }))
// OrderSummary receives discount via prop
child(OrderSummary(discountPercent = discountPercent.value))
}
}
}

Updating a component from outside Litho​

A component tree can be updated by creating a new root component with new props and calling ComponentTree#setRootAsync.

There are two main approaches for external systems to communicate with components:

Use StateFlow when you have reactive data streams. This is the preferred approach for Kotlin as it integrates naturally with coroutines and provides automatic lifecycle management. For example, a CartBadge can observe the cart item count from a repository:

class CartBadgeWithFlow(private val cartItemCountFlow: StateFlow<Int>) : KComponent() {
override fun ComponentScope.render(): Component {
val itemCount = useFlow(cartItemCountFlow)
return Text(text = "Cart (${itemCount ?: 0})")
}
}

With a listener pattern​

Use a listener pattern when working with callback-based APIs or when you need more control over subscription lifecycle:

interface CartUpdateListener {
fun addListener(listener: () -> Unit)

fun removeListener(listener: () -> Unit)
}
class CartBadge(private val cartListener: CartUpdateListener) : KComponent() {
override fun ComponentScope.render(): Component {
val itemCount = useState { 0 }

useEffect(cartListener) {
val listener: () -> Unit = { itemCount.update { it + 1 } }
cartListener.addListener(listener)
onCleanup { cartListener.removeListener(listener) }
}

return Text(text = "Cart (${itemCount.value})")
}
}

Communicating externally from a Component​

To send events from a component to a listener outside of the Litho hierarchy, define an observer interface and invoke it from a component event handler. For example, a PurchaseButton can notify an AnalyticsService when clicked:

interface AnalyticsService {
fun trackPurchaseClicked(productId: String, price: Double)
}
class PurchaseButton(
private val productId: String,
private val price: Double,
private val analyticsService: AnalyticsService,
) : KComponent() {
override fun ComponentScope.render(): Component {
return Text(
text = "Buy Now - $$price",
style = Style.onClick { analyticsService.trackPurchaseClicked(productId, price) },
)
}
}