Skip to content

Component Boilerplate Reduction #6

@BinaryMuse

Description

@BinaryMuse

Problem

Even trivial stateless components require:

  • Props struct with #[derive(Default)]
  • impl Component with type State = ()
  • At minimum render() + desired_height() (or view() 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions