Skip to content

Control flow syntax

control_flow demonstrates a core design of element!: the UI tree is part of ordinary Rust expressions. You can write if, if let, for, and match inside the tree, and different branches may return different element types without manually converting everything to AnyElement in application code.

control flow demo

This recording is generated by docs/tapes/control-flow.tape and runs examples/core/control_flow.rs. It moves the highlighted row, toggles the count between odd and even branches, and disables the optional name so if let falls through to else if.

cargo run --example control_flow

Press j/k to change the count and highlighted row, n to toggle the optional name, and q to exit.

The two sides of if / else do not have to be the same component type. This branch returns Text on one side and a bordered Border subtree on the other:

if count_value % 2 == 0 {
    Text(
        text: format!("if branch: count {count_value} is even"),
        style: Style::new().green().bold(),
    )
} else {
    Border(borders: Borders::ALL, height: Constraint::Length(3)) {
        Text(text: format!("else branch: count {count_value} is odd"))
    }
}

The macro converges those branches into element declarations the framework can reconcile. Each branch only needs to produce a valid element.

Optional data can be expanded directly inside the tree:

if let Some(name) = maybe_name {
    Text(text: format!("if let branch: optional name = {name}"))
} else if count_value > 4 {
    Text(text: "else if branch: no name, but count is high")
} else {
    Text(text: "else branch: no name yet")
}

This style works well for loading states, empty states, permission branches, and optional context. Do not first push an optional block into a temporary Vec just to render it; writing it directly in element! is usually clearer.

for can generate rows directly, but list items still need stable keys:

for (index, (label, detail)) in ROWS.iter().enumerate() {
    Text(
        key: index,
        text: format!("{label:<10} {detail}"),
    )
}

In the example, ROWS is a static array, so index is stable enough. In real business lists that can insert, delete, or reorder items, prefer business IDs as keys to avoid reusing state on the wrong row.

When page state is naturally an enum, step, or mode, match is easier to read than several booleans:

match selected_value {
    0 => { Text(text: "match branch 0: simple Text") }
    1 => { Border(borders: Borders::LEFT) {
        Text(text: "match branch 1: wrapped in a left border")
    } }
    2 => { Text(text: "match branch 2: rows come from a for loop") }
    _ => { Text(text: "match default: every branch can have its own element type") }
}

Each match arm wraps its element in { ... }; that keeps complex subtrees clear and avoids macro parsing ambiguity.

element! rebuilds lightweight declarations every frame; persistent state lives in reconciled Component instances. When control flow changes, the runtime uses ElementKey + TypeId to decide which nodes can be reused and which should be recreated.

Next, return to the component model to understand keys and instance reuse, or continue to Hooks to learn why state remains stable across branch changes and redraws.