-
Notifications
You must be signed in to change notification settings - Fork 0
Description
Problem
Even trivial stateless components require:
- Props struct with
#[derive(Default)] impl Componentwithtype State = ()- At minimum
render()+desired_height()(orview()if unified)
Near-term: simple_component! macro
A declarative macro for stateless components that produce an element tree:
simple_component!(Badge { label: String, style: Style } => |self| {
element! {
TextBlock {
Line { Span(text: self.label.clone(), style: self.style) }
}
}
});Expands to the struct definition, Default impl, and Component impl.
Deeper insight: hooks as capability declarations
The #[component] function model is more powerful than it first appears. If we think of the function as returning a definition of a component, then hooks don't just manage state and effects — they declare the component's full behavioral contract at runtime. Every trait method maps to a hook:
| Today (trait method) | Tomorrow (hook) |
|---|---|
render() + children() |
return value (Elements) |
desired_height() |
hooks.use_height_hint(n) |
handle_event() |
hooks.use_event(closure) |
handle_event_capture() |
hooks.use_event_capture(..) |
is_focusable() |
hooks.use_focusable(bool) |
cursor_position() |
hooks.use_cursor(pos) |
content_inset() |
Use View in the return tree |
layout() |
Use View in the return tree |
lifecycle() |
Hooks are the lifecycle |
This isn't just syntactic sugar over the trait — it's a different execution model where the component function IS the reconciliation unit. The struct + trait approach defines behavior statically (at compile time); the function + hooks approach defines it dynamically (at render time). The latter composes better and eliminates boilerplate.
Refined function component model
The #[component] attribute declares the full contract: props, state, children type, and event type. The function receives props and state as explicit parameters; hooks are for behavioral declarations, not data storage.
#[props]
struct MyProps {
pub title: String,
pub borders: Option<Borders>,
#[default(true)]
pub autofocus: bool,
}
#[derive(Default)]
struct MyState {
input_text: String,
}
enum MyEvent {
Submit(String),
}
#[component(props = MyProps, state = MyState, event = MyEvent)]
fn MyComp(props: &MyProps, state: &Tracked<MyState>, hooks: &mut Hooks) -> Elements {
let children = hooks.use_children();
hooks.use_event_capture(|event, ctx| {
if is_enter_key(event) {
ctx.emit(MyEvent::Submit(ctx.state::<MyState>().input_text.clone()));
return EventResult::Consumed;
}
EventResult::Ignored
});
element! {
Card(title: props.title.clone()) {
#(children)
View {
Input(placeholder: "Gimme words")
Text(style: Style::default().fg(Color::Gray)) {
"Press [Enter] to submit"
}
}
}
}
}Key design decisions:
State is a declared type, not use_state(). A concrete State struct forces you to design state as a coherent unit. React's use_state proliferation (8 independent atoms in one component) makes the state shape invisible. A single Tracked<S> with one dirty flag is also simpler — granular per-atom tracking doesn't help in eye-declare's model since components re-render as a whole unit.
Props are a function parameter, not self. Avoids the &self in callbacks problem — callbacks get a framework-provided ctx with typed access to state and emit, no need to capture &self.
Hooks are for behavioral capabilities, not data. use_event, use_focusable, use_height_hint, use_interval etc. declare what the component does. State storage is the State type. This is a cleaner separation than "hooks for everything."
#[props] with required fields and #[default]. Eliminates the Default requirement. A builder pattern verifies required fields at compile time. Independently useful regardless of the component model.
use_state still useful for derived/local values. Like React's useRef — a memoized computation or local accumulator that doesn't need to persist across component identity changes. But it's a refinement, not the primary state pattern.