Skip to content

Composition

Formula supports composition — a parent can run child formulas within evaluate() via context.child(formula, input), passing data and callbacks down through Input and receiving the child's Output.

override fun Snapshot<Input, State>.evaluate(): Evaluation<Output> {
    val listOutput = context.child(itemListFormula, childInput)
    return Evaluation(
        output = Output(list = listOutput)
    )
}

Passing Input to Children

Input and Re-evaluation

The parent creates a new Input for each child every time evaluate() runs. Evaluation is triggered when the parent's own input, its state or a child's output changes. This is how data flows to children — when the parent's state updates, the child receives new Input reflecting those changes.

val listOutput = context.child(
    formula = itemListFormula,
    input = ItemListFormula.Input(
        items = state.items,
        onItemSelected = context.onEvent<ItemId> { itemId ->
            transition(state.copy(selectedItemId = itemId))
        }
    )
)

Input Equality

A child only re-evaluates if its input actually changed. Formula compares the new input to the previous one using equals(). If they're equal, the child skips re-evaluation and context.child() returns the previously computed output.

Listener Stability

When a parent creates a lambda inline in evaluate(), a new instance is created every evaluation. Since lambdas use identity equality, the child's input equality check always fails — causing unnecessary re-evaluation even when nothing actually changed. context.onEvent and context.callback solve this by maintaining the same instance across parent re-evaluations — the runtime matches them via composite key in LifecycleCache and updates the internal transition, but the instance stays the same.

Child Lifecycle

Child formulas declared via context.child() are started by the runtime and persist across parent re-evaluations — the child keeps its state as long as it remains declared. A child only re-evaluates if its input changed or state changed within its own hierarchy.

The runtime needs to match children across evaluations to maintain their state. It uses a composite key of formula type + formula.key(input) to identify each child. As long as the key stays the same, the child persists with its existing state. If key(input) changes, the runtime treats it as a different child — terminates the old instance and starts a new one with initialState.

Conditional Children

Since children are declared in evaluate(), conditional logic controls their existence.

val dialog = if (state.showDialog) {
    context.child(formula = dialogFormula, input = Unit)
} else {
    null
}

When the condition becomes false, the child is terminated — its state is lost and its actions are cancelled. When the condition becomes true again, a fresh child starts with initialState.

Formula Key

Override formula.key(input) when a parent runs multiple instances of the same formula type. For example, rendering a list of items where each item is managed by the same formula:

state.items.map { item ->
    context.child(
        formula = itemFormula, 
        input = ItemFormula.Input(itemId = item.id),
    )
}

Without a key, the runtime cannot distinguish between instances. Override key(input) to provide a unique identity:

override fun key(input: Input) = input.itemId

When the key changes, the runtime terminates the old instance and starts a new one with fresh initialState.