BlincComponent Macro

BlincComponent generates reactive props and persistent Signal hooks from a struct. Props accept ordinary values, Signal<T> and Computed<T> through IntoReactive<T>. Forward them to UI bindings so changes patch mounted elements.

Declare the Fields

#![allow(unused)]
fn main() {
use blinc::app::prelude::*;

#[derive(BlincComponent)]
pub struct Counter {
    /// Text displayed before the count.
    pub label: String,
    pub count: i32,
    pub disabled: bool,
}
}

This generates CounterProps, Counter::props(label, count, disabled) and fluent setters on the props. The original Counter struct stays unchanged; its fields describe the prop types and named hooks.

The companion has the same visibility and generic parameters as the declared struct. Each prop keeps its field visibility and documentation, and stores Reactive<T>. Cloning props retains their Signal and Computed sources.

Supply Reactive Props

#![allow(unused)]
fn main() {
use blinc::app::prelude::*;
use blinc::core::reactive::{computed, signal};

#[derive(BlincComponent)]
pub struct Status {
    pub label: String,
    pub disabled: bool,
}

fn status(props: StatusProps) -> Div {
    div()
        .class("status")
        .attr_when("disabled", "", props.disabled)
        .child(Text::bound(props.label).class("status-label"))
}

fn example() -> Div {
    let busy = signal(false);
    let label = computed(move |g| {
        if g.get(busy).unwrap_or(false) {
            "Working".to_string()
        } else {
            "Ready".to_string()
        }
    });
    let props = Status::props("Initial", false)
        .label(label)
        .disabled(busy);
    status(props)
}
}

The constructor accepts every ordinary field in declaration order. Setters accept the same impl IntoReactive<T> inputs, including borrowed signals, borrowed computeds and an existing Reactive<T> from another props value.

Status::props("Ready", false) uses constants. Passing busy keeps its signal identity; passing label keeps its computation. No value is read or copied into a new signal while constructing props.

The status class styles the root, status-label styles its text, and the root's disabled attribute exposes the bound state for restyling:

.status { padding: 12px; border-radius: var(--radius-md); }
.status-label { color: var(--text-primary); }
.status[disabled] { opacity: 0.5; }

The derive generates data APIs. Declare classes and CSS rules in the component that consumes the props.

Own Mutable Values with Signal Hooks

#![allow(unused)]
fn main() {
use blinc::app::prelude::*;
use blinc::app::windowed::WindowedContext;

#[derive(BlincComponent)]
pub struct Counter {
    pub label: String,
    pub count: i32,
    pub disabled: bool,
}

fn counter(ctx: &WindowedContext) -> Div {
    let count: Signal<i32> = Counter::use_count(ctx, 0);
    let caption = blinc::core::reactive::computed(move |g| {
        format!("Count: {}", g.get(count).unwrap_or(0))
    });
    let props = Counter::props(caption, count, false);
    div()
        .class("counter")
        .attr("tabindex", "0")
        .attr_when("disabled", "", props.disabled)
        .child(Text::bound(props.label))
        .on_click(move |_| count.update(|value| value + 1))
}
}

use_count creates a writable Signal<i32> on its first call and retrieves that same signal on later builds. Its initial argument is a plain i32, used only to seed owned data. External Signal or Computed inputs belong in Counter::props, whose fields are read-only binding sources.

Generated hookIdentity
Counter::use_count(ctx, initial)Component type and field
Counter::use_count_for(ctx, key, initial)Component type, field and explicit instance key
Counter::use_count_auto(ctx, initial)Component type, field and caller location

Use _for in lists and loops. Two calls to _auto from one loop body share a call site. Different fields, modules, component types and generic instantiations have separate hook identities.

Hooks work with any BlincContext; they do not require a native window.

Combine Props in Computed and Effect

Use Reactive::read(g) inside computations so dependencies stay tracked. Use now() only when taking a snapshot, such as reading a prop in an event handler. Reading a prop while building a view does not create a binding.

#![allow(unused)]
fn main() {
use blinc::app::prelude::*;
use blinc::core::reactive::{computed, effect};
use blinc::layout::binding::IntoReactive;

#[derive(BlincComponent)]
pub struct Counter {
    pub label: String,
    pub count: i32,
}

fn counter(props: CounterProps) -> Div {
    let count = props.count.clone();
    let _observer = effect(move |g| {
        tracing::debug!(count = count.read(g).unwrap_or(0), "Counter changed");
    });
    let caption = computed(move |g| {
        format!("{}: {}",
            props.label.read(g).unwrap_or_default(),
            props.count.read(g).unwrap_or(0))
    });
    div().class("counter").child(Text::bound(caption.into_reactive()))
}
}

Use Computed for values derived from props, Effect for side effects, and UI bindings for visual updates. Components built under a build scope share its cleanup lifetime. Ordinary prop changes do not need .deps() or an on_state rebuild.

Animation Hooks and Unit Structs

#[animation] fields retain the existing spring hooks and are omitted from props. For example, a component with label: String and #[animation] scale: f32 receives Component::props(label) and use_scale(ctx, initial, config) returning SharedAnimatedValue.

Unit and tuple structs generate component keys and shared spring/timeline hooks. A unit struct has no companion props type:

#![allow(unused)]
fn main() {
use blinc::app::prelude::*;
use blinc::app::windowed::WindowedContext;

#[derive(BlincComponent)]
struct LoadingIndicator;

fn animation(ctx: &WindowedContext) {
    let _spring = LoadingIndicator::use_animated_value(ctx, 1.0, SpringConfig::snappy());
    let _timeline = LoadingIndicator::use_animated_timeline(ctx);
}
}

All component types expose COMPONENT_KEY, instance_key() and instance_key_for(suffix). Shared spring and timeline methods also have _with(ctx, suffix, ...) variants. Prefer CSS transitions, keyframes and motion() for component appearance and interaction states.

Dependencies and Migration

Consumers can use the blinc package alone, including when renamed in Cargo.toml. The derive resolves its core, animation and layout modules. Direct dependencies still work: blinc_macros (or the derive reexport from blinc_app), blinc_core, blinc_animation and, for named reactive fields, blinc_layout.

Existing field hooks now return Signal<T>. Use its get, set and update methods and forward the signal to a binding. Hooks retain their existing keys for module-level non-generic component types. Ordinary Rust struct literals keep their declared field types; use the generated Name::props(...) factory when supplying reactive inputs.