Structure
component
Declare a view with its own scope for state, built once by a closure.
componentuse crownui::prelude::*;
fn counter() -> impl View {
component(|cx| {
let count = cx.signal(0);
hstack((
button("-1").on_click(move |cx, _| count.update(cx, |value| *value -= 1)),
text(bind(move |rt| format!("Count: {}", count.get(rt)))),
button("+1").on_click(move |cx, _| count.update(cx, |value| *value += 1)),
))
.gap(8.0)
.items_center()
})
}pub fn component<V: View, F: FnOnce(&mut Cx) -> V + 'static>(build: F) -> Component<F>component declares a view with its own scope for state. Its closure runs once, when the view is built.
Parameters
Creates state with cx.signal, cx.memo, cx.effect or cx.spawn and returns the view to show.
It runs exactly once.
Returns
A view. It implements View and nothing else.
Component has no style, event or semantic methods. Style the view you return inside the closure.
// Does not compile: Component has no style methods.
// component(|_cx| text("Hi")).p(8.0)
component(|_cx| text("Hi").p(8.0))What the scope owns
build creates a scope and makes it current while the closure runs and while the returned view
mounts. Everything created in that time belongs to the scope:
- signals, memos and stores from
cx.signal,cx.memoandcx.store - effects from
cx.effect - async tasks from
cx.spawn - listeners added by the views inside
The scope lives as long as the component stays mounted. When a show branch, a keyed branch or a
for_each row that contains the component unmounts, CrownUI disposes the scope. Effects stop, tasks
are dropped and signals are freed.
Pass arguments with a move closure
A component is usually the body of a function. Capture the function's arguments with move.
fn stepper(title: &'static str, start: i32) -> impl View {
component(move |cx| {
let value = cx.signal(start);
hstack((
text(title).grow(1.0),
button("-").on_click(move |cx, _| value.update(cx, |value| *value -= 1)),
text(bind(move |rt| value.get(rt).to_string())).w(32.0).text_center(),
button("+").on_click(move |cx, _| value.update(cx, |value| *value += 1)),
))
.gap(8.0)
.items_center()
})
}Signal handles are Copy, so a component can also receive a parent's signal and share it.
fn volume_row(volume: Signal<i32>) -> impl View {
component(move |cx| {
let muted = cx.signal(false);
hstack((
toggle(muted).label("Mute"),
text(bind(move |rt| {
if muted.get(rt) { "Muted".to_string() } else { format!("{}%", volume.get(rt)) }
})),
))
.gap(8.0)
})
}Read first, then write
Cx derefs to the reactive runtime, so a read and a write both borrow cx mutably. Read into a
local first, then create or write.
#[derive(Clone)]
struct Settings {
dark_mode: bool,
}
fn dark_mode_row(settings: Signal<Settings>) -> impl View {
component(move |cx| {
let initial = settings.with(cx, |settings| settings.dark_mode);
let dark = cx.signal(initial);
toggle(dark).label("Dark mode")
})
}Why it runs once
The closure is not a render function. CrownUI does not call it again when state changes, and it
does not diff its output. Each bind, Signal or Memo you pass to a property becomes one small
observer that updates one property in place. An update costs work in proportion to the properties
that read the changed signal, not to the size of the component.
Each binding also keeps a closure and an observer slot alive for as long as the component is mounted, so memory grows with the number of bindings.