# Reactive State
mirui has a reactive state layer — signals, computed values, and effects —
that drives widget attributes and structure declaratively from the `ui!`
macro. Change a signal and the widgets that read it update; no manual
diffing, no observer wiring.
This guide is the long-form companion to the `mirui::core::reactive` API docs and
the State demos in the gallery.
## Contents
1. [Primitives](#primitives)
2. [Reactive attributes](#reactive-attributes)
3. [Layout-responsive attributes](#layout-responsive-attributes)
4. [Reactive control flow](#reactive-control-flow)
5. [Lists: `walk`, index vs keyed](#lists)
6. [The flush model](#the-flush-model)
7. [Limits](#limits)
## Primitives
Three types live in `mirui::core::reactive`:
```rust
use mirui::core::reactive::{Signal, Computed, Effect};
let count = Signal::new(0i32); // holds a value, tracks readers
count.set(1); // replace
let count = count.clone();
Computed::new(move || count.get() * 2) // lazy, recomputes when count changes
};
});
```
- `Signal<T>` is the source of truth. `get()` / `with()` subscribe the
current reader (an effect, a computed, or a reactive binding). `set()` /
`update()` mark subscribers dirty.
- `Computed<T>` derives a value lazily: it only recomputes when one of its
inputs changed, and only when read.
- `Effect` runs a closure now and again whenever the signals it read change.
Signals are `Clone` (cheap, reference-counted), so clone one into each
closure that needs it.
## Reactive attributes
Inside `ui!`, prefix an attribute value with `$` to bind it reactively. A
bare path reads the signal; a `${ … }` block runs an expression:
```rust
let label = Signal::new(0i32);
ui! {
:(
parent: root
world: &mut world
:)
Text (text: ${ alloc::format!("Count: {}", label.get()) }, height: 40)
}
```
When `label` changes, only that attribute updates — the widget is not
rebuilt. Reactive binding is supported on `text`, `path`, `bg_color`,
`text_color`, `width`, and `height`.
`attr: $signal` is shorthand for `attr: ${ signal.get() }`.
A reactive text path keeps the text entity stable while transferring its subscription to the selected path:
```rust
let selected = Signal::new(first_path);
ui! {
Text("Live route", path: $selected)
}
```
To derive the geometry of one mutable path from signals, use `UiScope::bind_path`. The owner-bound effect edits the existing `PathId`, retains its allocation, and invalidates drawing and text consumers when its revision changes. See [`typography.md`](typography.md#derive-path-geometry-from-signals).
## Layout-responsive attributes
`$` reacts to application state. `@` reacts to computed layout geometry and lists every dependency in its head:
```rust
ui! {
Column (container: true) {
Text(
"Status",
font_size: @width {
if width < Fixed::from_int(520) { 18_u16 } else { 30_u16 }
},
padding: @(width, height) {
Padding::all(width.min(height) / 24)
}
)
View(
id: "stage",
width: Dimension::percent(100),
height: 180
)
Text(
"Stage label",
width: @id(stage).width { stage.width },
top: @id("stage").height as stage_height { stage_height / 8 }
)
}
}
```
Bare `width` and `height` resolve to the nearest ancestor marked `container: true`. Named dependencies use the existing widget ID registry. String IDs require `as alias`; aliases are also available for identifier IDs and container geometry. One widget can declare up to 4 distinct dependencies, stored without a runtime allocation. Multiple responsive attributes on the same widget share one binding and one geometry snapshot.
The binding runs after layout and may request one additional layout pass when it changes a layout property. The pass count is bounded, and unchanged derived values do not invalidate the widget. Continuous geometry that cannot be expressed as a widget property can use `UiScope::bind_layout` with explicit `LayoutDependency` values and caller-owned state.
## Reactive control flow
A `$` on a control-flow head makes the branch reactive: when the head's
signals change, the subtree is rebuilt.
```rust
let show = Signal::new(true);
let other = Signal::new(false);
let state = Signal::new(Load::Loading); // your own enum
ui! {
:(
parent: root
world: &mut world
:)
Column (grow: 1.0) {
if ${ show.get() } {
Text ("visible")
} elif ${ other.get() } {
Text ("alt")
} else {
Text ("hidden")
}
match ${ state.get() } {
Load::Loading => {
Text ("loading")
}
Load::Ready(s) => {
Text (s)
}
}
}
}
```
- `if $cond` / `elif` / `else` swap one single-root branch in place.
- `match $expr` selects one arm and rebuilds it on change.
- `elif` is a single keyword (not `else if`).
- A head **without** `$` is static — evaluated once at build, never re-run.
## Lists
`walk` iterates a collection. With `$` it re-evaluates when the iterable's
signals change:
```rust
let items = Signal::new(alloc::vec![/* … */]);
Column (grow: 1.0) {
walk ${ items.get() } with item {
Text (item.name, bg_color: item.color, height: 28)
}
}
```
Two reconciliation strategies:
- **Index-based** (default, no `by`): rows align by position. Growing the
list builds and appends new tail rows; shrinking despawns tail rows;
surviving rows keep their entity. Correct for append / drop-tail lists.
- **Keyed** (`by <key>`): rows align by identity. When the list reorders or
an item is inserted/removed in the middle, a row keeps its entity (and any
per-widget state) and just moves, rather than being rebuilt in place.
```rust
walk ${ items.get() } with item by item.id {
Text (item.name, height: 28)
}
```
Use keyed when the list reorders or mutates in the middle; index-based is
lighter for plain append/drop-tail.
## The flush model
Setting a signal does not update widgets immediately. It marks subscribers
dirty and enqueues them. Once per frame, after systems and before render,
`flush_signal_dirty` drains the queue: dirty effects re-run, dirty widgets
get re-rendered. Reactivity is tick-driven, consistent with the rest of the
framework — there is no background thread.
A reactive binding's first run applies its initial value at construction
(inside the `ui!` build), so the first frame already shows the correct
state.
## Limits
- **Single-root reactive branches**: each `if` / `match` / `walk` reactive
branch produces one top-level widget, matching SolidJS / Leptos. Wrap
multiple widgets in a container.
- **Reactive blocks mount after static siblings**: a reactive `if` / `match`
inside a container whose other children are static appears after them on
first build, regardless of source order. The branch keeps its position
across swaps thereafter.
- **Index-based `walk` does not update surviving rows' content**: with no
`by` key, a middle insert/remove shifts which data each surviving row
shows only through that row's own reactive attributes; the row structure
itself is not re-matched. Use keyed `walk` when identity matters.