Components
Writing reusable pieces of interface — the component macro, props, defaults, conversions, and what a component costs.
A component is a Rust function that builds a piece of interface. #[component] turns the function
into something a view can call, with named arguments and compile-time checks on them. This page
assumes Views and Signals.
The smallest one
use zgui::prelude::*;
/// A labelled value.
#[component]
fn Stat(
/// What the value is called.
#[prop(into)]
label: String,
/// The value, which follows whatever produced it.
value: Signal<i32>,
) -> impl IntoView {
view! {
row(class = "stat") {
label(class = "stat__label") {{label}}
text(class = "stat__value") {{move || value.get().to_string()}}
}
}
}Call it like any other node, with its arguments written as attributes:
view! {
column {
Stat(label = "Used", value = used.into())
Stat(label = "Free", value = free.into())
}
}The rules on the function
#[component] refuses a function it cannot turn into a component. Each error names the rule.
| Rule | Why |
|---|---|
| The name is upper camel case. | A lower-case name in a view is an element. thing() is an element; Thing() is a component. |
It has a return type, normally -> impl IntoView. | A component returns a view. |
It is a free function and takes no self. | Props are named arguments, not fields. |
| It has no lifetime parameters. | Props outlive the call that built them. |
| Each argument is one identifier. | A prop is named, so it cannot be a pattern. |
No argument has an impl Trait type. | The prop is stored in a struct, and impl Trait cannot be. Name a type parameter instead. |
Type and const generic parameters are fine, where clauses included:
#[component]
fn Twice<F>(
/// Called once per repetition, with the index.
children: F,
) -> impl IntoView
where
F: Fn(usize) -> AnyView + 'static,
{
view! { {children(0)}{children(1)} }
}Props
Every argument is a prop, and every prop is written by name at the call site. Order does not matter.
Required by default
A prop with no attribute is required. Leaving it out is a compile error that names it:
error[E0277]: `Thing` is missing the required prop `label`
--> src/main.rs:21:21
21 | let _ = view! { Thing(times = 2) };
| ^^^^^ `label` was never givenThe check is a typestate on the generated builder, so it happens at compile time and never at run time.
The five prop attributes
Prop
Type
They combine: #[prop(into, optional)], #[prop(into, default = 1)],
#[prop(into, name = "type")].
/// A labelled value.
#[component]
fn Field(
/// The label.
#[prop(into)]
label: String,
/// Shown after the label when there is one.
#[prop(into, optional)]
hint: Option<String>,
/// How many columns the field spans.
#[prop(default = 1)]
span: u8,
) -> impl IntoView {
view! { {label}{hint}{span.to_string()} }
}
// `hint` and `span` may be left out; `label` may not.
view! { Field(label = "Name") }
view! { Field(label = "Address", hint = "optional", span = 2) }A prop declared Option<T> takes a T at the call site. Write hint = "optional", not
hint = Some("optional"). It also accepts an Option<T>, which is what lets a wrapper component
forward its own optional prop straight through.
Only #[prop(...)] and doc comments are allowed on an argument. Doc comments are re-emitted on both
the generated struct field and the builder setter, so they appear in rustdoc where a caller looks.
What a setter accepts
The declared type decides what the caller may write:
| Declared type | The call site accepts |
|---|---|
T | a T |
T with #[prop(into)] | anything Into<T> |
Option<T>, optional | a T or an Option<T> |
Children | a children block, taken once. See Children and slots |
ChildrenFn | a children block, buildable more than once |
ReactiveValue<T> with #[prop(into)] | a constant, a signal, a memo or a closure |
The last row is how a component takes "anything readable as a T" without caring which. See
Derived state.
What a component costs
The function body runs once. When the view is built, Stat(...) is called, it builds its part
of the document, and it returns. Changing value afterwards does not call Stat again. It re-runs
only the closure inside it that read value.
There is no diffing, no virtual tree and no reconciliation pass. A component is not a render function; it is a constructor.
This has one consequence worth stating outright: a prop is a value, taken once. If a prop is
i32, the component sees the number that was passed at build time and will never see another. To
give a component something that changes, pass a signal, a memo or a closure — and read it inside a
reactive hole.
#[component]
fn Bad(count: i32) -> impl IntoView {
// `count` was read once. This text never changes.
view! { text {{count.to_string()}} }
}
#[component]
fn Good(count: Signal<i32>) -> impl IntoView {
// Read inside a closure, so it follows whatever produced it.
view! { text {{move || count.get().to_string()}} }
}Scope and cleanup
Every component gets its own reactive scope. Signals, contexts, stored values and effects created in the body belong to it.
When the component is removed from the interface — a branch flipped, a row deleted — that scope is disposed of synchronously, in the same frame. Its effects stop, its stored values are dropped, and its cleanup closures run. Nothing is left behind and nothing waits for a later collection pass.
This is why a signal has to be created inside a component body and not in a static: the body is
inside an owner, and the owner is what makes the disposal happen. See
Context.
Forwarding attributes
A component that wraps an element often wants to let its caller add classes, listeners or
accessibility attributes to that element. #[prop(attrs)] collects them, and the spread replays
them:
#[component]
fn Button(
/// Whatever the caller forwarded.
#[prop(attrs)]
attrs: Attrs,
children: Children,
) -> impl IntoView {
view! {
control(class = "button", {..attrs}) {
{children.into_view_once()}
}
}
}view! {
Button(class = "button--primary", on:click = save) {"Save"}
}Attributes written before the spread apply first and attributes after it apply last, so position
decides who wins. At most one prop per component may be #[prop(attrs)]; the setter is always
called attrs.
Naming and files
A component is a normal Rust item. Put one per file, or several in a module — whatever suits. The
generated props struct and builder take the component's own visibility, so pub fn Card gives a
pub component and fn Card keeps it private to the module.
App is a natural name for a root component, which is why the entry point is the function app()
and not a type called App. Nothing a component declares can be spelled app, because
#[component] requires a capital.