-
Notifications
You must be signed in to change notification settings - Fork 0
Guide Components and State
These two ideas — components and state — are the whole framework. Everything else is widgets and platform glue on top. Get these and you can build anything.
A component is a struct that implements one method:
impl Component for MyScreen {
fn build(&self, ctx: &mut Context) -> Element {
// describe the UI here
}
}build returns an Element — a description of your UI as a tree of widgets. The framework calls build for you; you never call it yourself. Think of it as: "given the current state, here's what the screen should look like."
Components compose: a component's build can include child components and widgets freely.
A build that only ever returns the same thing is a static screen. To make it react, you read state through ctx:
let count = ctx.state(0i32); // an Atom<i32>, starting at 0ctx.state(default) returns an Atom — a reactive value. Atoms have three moves:
count.get(); // read the current value
count.set(5); // replace it
count.update(|n| n + 1); // derive the next from the currentThe magic: when you set or update an atom, every component that read it is scheduled to rebuild — and only those components. You never manually refresh the UI; you change state, and the affected parts redraw.
Putting it together — a button that increments a number on screen:
use rosace::prelude::*;
struct Counter;
impl Component for Counter {
fn build(&self, ctx: &mut Context) -> Element {
let count = ctx.state(0i32);
Scaffold::new(
Column::new()
.padding(EdgeInsets::all(24.0))
.spacing(12.0)
.child(Text::new(format!("Count: {}", count.get())))
.child(Button::new("Increment").on_press({
let count = count.clone();
move || count.update(|n| n + 1)
})),
)
.into_element()
}
}
fn main() {
App::new().title("Counter").size(400, 300).launch(Counter);
}What happens when you click:
-
on_pressrunscount.update(|n| n + 1). - That marks
Counterdirty and requests a frame. - Next frame, the framework re-runs
Counter::build—count.get()is now1— and repaints just this component.
Note the let count = count.clone() before the closure: the button's handler outlives this build call, so it needs its own handle to the atom. Atoms are cheap to clone (they share the underlying value).
State is matched by call order within build — the first ctx.state is slot 0, the second is slot 1, and so on (if you've used React hooks, this is exactly useState). So:
- ✅ Call
ctx.state(...)unconditionally, in a stable order, at the top ofbuild. - ❌ Never put
ctx.state(...)inside anifor a loop — it shifts every later slot and scrambles your state.
ctx.state lives for the life of the running app. For values that must survive a full quit-and-relaunch (a login token, a preference, a draft), use ctx.state_permanent:
let launches = ctx.state_permanent("launch_count", 0i64);It behaves like a normal atom, but its value is stored on disk (keyed by the string you pass) and restored on the next launch. More in Persistence & Networking.
Under the hood: how "changing an atom rebuilds only its subscribers" actually works is in the architecture book — Core: Component, Element, Context.
Next: Layout & Widgets.