Props & State

Props

Props are typed inputs set by the parent. They are immutable within the component.

tape
component Display {
    prop value: *u8;
    prop size: i64 = 16;
    prop bold: bool = false;
}

The parent sets props at instantiation:

tape
Display { value: "Hello"; size: 24; }
Display { value: count_text; }  // uses default size and bold

Rules:

  • Props with defaults are optional at instantiation
  • Props without defaults are required (compiler error if missing)
  • Props are read-only inside the component
  • Props can be any type: primitives, structs, pointers, function pointers, color
  • Props are style-targetable — #style can override default values

Props and #style

Props can be set from #style blocks, overriding defaults without the parent explicitly passing them:

tape
component Button {
    prop bg: color = #444444;
    prop fg: color = #FFFFFF;
    prop label: *u8;
}
tape
#style
Button {
    bg: #336699;
    fg: #FCFDFF;

    :hovered { bg: #4477AA; }
    :active { bg: #225588; }
}

The #style values act as overrides to the prop defaults. The parent can still explicitly set a prop at instantiation, which takes priority over the style.

Reactive state (var)

Mutable var declarations inside a component are reactive state. Any mutation marks the component dirty for re-render on the next frame:

tape
component Counter {
    var count: i64 = 0;
    var count_text: *u8 = "0";

    fn increment() {
        count = count + 1;
        count_text = str.i64_to_cstr(count);
    }
}

State initializers run once at component creation and can reference props.

Auto-generated setters

For every var field, the compiler generates a set_<name> method that stores the value and marks dirty:

tape
component Slider {
    var value: f64 = 0.0;
    // compiler generates: fn set_value(val: f64) { value = val; /* mark dirty */ }
}

These are useful for binding to events:

tape
widgets.TextInput { value: query; on_change: set_query; }

If you define fn set_<name> manually, the compiler skips auto-generation for that field.

Dirty marking

The compiler inserts dirty flag writes after every var assignment inside a component method. Multiple mutations in one handler are idempotent — the component is repainted at most once per frame:

  1. Event handler executes (may mutate state multiple times)
  2. Handler returns
  3. Frame tick: runtime walks dirty components, repaints dirty regions
  4. Clear dirty flags

Interaction state (state)

state declares boolean flags specifically for interaction styling. Unlike var, these are targetable from #style via :name pseudo-selectors:

tape
component Button {
    state hovered: bool = false;
    state active: bool = false;
    state disabled: bool = false;

    fn handle_pointer(ev: *ui.PointerEvent) {
        if (ev.kind == ui.PointerKind.Enter) { hovered = true; }
        if (ev.kind == ui.PointerKind.Leave) { hovered = false; }
        if (ev.kind == ui.PointerKind.Down) { active = true; }
        if (ev.kind == ui.PointerKind.Up) { active = false; }
    }
}

Rules:

  • state fields must be bool
  • Mutations mark dirty (same as var)
  • Targetable from #style with :name pseudo-selectors
tape
#style
Button {
    bg: #444444;
    :hovered { bg: #555555; }
    :active { bg: #333333; }
    :disabled { bg: #222222; fg: #666666; }
}

Here bg and fg target props (overriding their values), while :hovered, :active, and :disabled target state fields (conditional styling when that flag is true). The compiler verifies each :name matches a state declaration — unmatched selectors are a compile error.

Props vs var vs state

propvarstate
Set byParent / #styleComponentComponent
Mutable insideNoYesYes
Marks dirtyWhen parent changesOn any mutationOn any mutation
TypeAnyAnybool only
Style-targetableYes (value override)NoYes (:name selector)
Auto-setterNoYes (set_<name>)No

Last modified: