Skip to content
This repository was archived by the owner on Aug 27, 2026. It is now read-only.

Latest commit

 

History

History
1269 lines (1021 loc) · 28.9 KB

File metadata and controls

1269 lines (1021 loc) · 28.9 KB

RxTUI Documentation

RxTUI is a reactive terminal user interface framework for Rust that brings modern component-based architecture to the terminal. It combines React-like patterns with efficient terminal rendering through virtual DOM diffing.

Table of Contents

Getting Started

Add RxTUI to your Cargo.toml:

[dependencies]
rxtui = "0.1"
tokio = { version = "1.0", features = ["full"] }  # Required for async effects

Note: The effects feature is enabled by default. To disable it:

[dependencies]
rxtui = { version = "0.1", default-features = false }

Create your first app:

use rxtui::prelude::*;

#[derive(Component)]
struct HelloWorld;

impl HelloWorld {
    #[view]
    fn view(&self, ctx: &Context) -> Node {
        node! {
            div(bg: blue, pad: 2, @key_global(esc): ctx.handler(())) [
                text("Hello, Terminal!", color: white, bold),
                text("Press Esc to exit", color: white)
            ]
        }
    }
}

fn main() -> std::io::Result<()> {
    App::new()?.run(HelloWorld)
}
• • •

Terminal Modes

RxTUI supports two terminal rendering modes: Alternate Screen (default) and Inline.

Alternate Screen Mode (Default)

The default mode uses the terminal's alternate screen buffer. This is ideal for full-screen applications:

fn main() -> std::io::Result<()> {
    App::new()?.run(MyComponent)  // Uses alternate screen
}

Characteristics:

  • Takes over the full terminal screen
  • Content disappears when the app exits
  • Best for interactive applications, editors, dashboards

Inline Mode

Inline mode renders directly in the terminal buffer without switching screens. Content persists after the app exits, making it ideal for CLI tools:

fn main() -> std::io::Result<()> {
    // Simple inline mode with defaults
    App::inline()?.run(MyComponent)?;

    // This prints after the UI since content is preserved
    println!("Done! The UI above is preserved.");
    Ok(())
}

Characteristics:

  • Renders in the main terminal buffer
  • Content persists in terminal history after exit
  • Height is content-based by default (grows to fit)
  • Mouse capture disabled by default (allows terminal scrolling)

Custom Inline Configuration

For fine-grained control over inline rendering:

use rxtui::{App, InlineConfig, InlineHeight};

fn main() -> std::io::Result<()> {
    let config = InlineConfig {
        // Fixed height of 10 lines
        height: InlineHeight::Fixed(10),
        // Show cursor during rendering
        cursor_visible: true,
        // Preserve output after exit
        preserve_on_exit: true,
        // Don't capture mouse (allow terminal scrolling)
        mouse_capture: false,
    };

    App::inline_with_config(config)?.run(MyComponent)
}

Height Modes

Control how inline mode determines rendering height:

// Fixed number of lines
InlineHeight::Fixed(10)

// Grow to fit content, with optional maximum
InlineHeight::Content { max: Some(24) }  // Max 24 lines
InlineHeight::Content { max: None }       // No limit (default)

// Fill remaining terminal space below cursor
InlineHeight::Fill { min: 5 }  // At least 5 lines
• • •

Components

Everything in RxTUI is a component. Think of them as self-contained UI pieces that know how to manage their own state and behavior. Components have three main capabilities: handling events (through update), rendering UI (through view), and running async operations (through effect):

Basic Component

#[derive(Component)]
struct TodoList;

impl TodoList {
    #[update]
    fn update(&self, ctx: &Context, msg: TodoMsg, mut state: TodoState) -> Action {
        // Messages come here from events in your view
        // You update state, then return Action::update(state) to re-render
    }

    #[view]
    fn view(&self, ctx: &Context, state: TodoState) -> Node {
        // This renders your UI using the current state
        // Uses the node! macro to build the UI tree
    }

    #[effect]
    async fn fetch_todos(&self, ctx: &Context, state: TodoState) {
        // Async effects for background tasks
        // Useful for timers, API calls, or any async operation
    }
}

Component Trait

The #[derive(Component)] macro automatically implements the Component trait. You can also implement it manually:

impl Component for MyComponent {
    fn update(&self, ctx: &Context, msg: Box<dyn Message>, topic: Option<&str>) -> Action {
        // Handle messages
    }

    fn view(&self, ctx: &Context) -> Node {
        // Return UI tree
    }

    fn effects(&self, ctx: &Context) -> Vec<Effect> {
        // Return async effects
    }
}

Complete Working Example

Here's a complete working example of a stopwatch component with async effects:

use rxtui::prelude::*;

#[derive(Component)]
struct Stopwatch;

impl Stopwatch {
    #[update]
    fn update(&self, _ctx: &Context, tick: bool, state: u64) -> Action {
        if !tick {
            return Action::exit();
        }
        Action::update(state + 10)
    }

    #[view]
    fn view(&self, ctx: &Context, state: u64) -> Node {
        let seconds = state / 1000;
        let centiseconds = (state % 1000) / 10;

        node! {
            div(
                pad: 2,
                align: center,
                w_frac: 1.0,
                gap: 1,
                @key(esc): ctx.handler(false),
                @char_global('q'): ctx.handler(false)
            ) [
                richtext[
                    text("Elapsed: ", color: white),
                    text(
                        format!(" {}.{:02}s ", seconds, centiseconds),
                        color: "#ffffff",
                        bg: "#9d29c3",
                        bold
                    ),
                ],
                text("press esc or q to exit", color: bright_black)
            ]
        }
    }

    #[effect]
    async fn tick(&self, ctx: &Context) {
        loop {
            tokio::time::sleep(std::time::Duration::from_millis(10)).await;
            ctx.send(true);
        }
    }
}

fn main() -> std::io::Result<()> {
    App::new()?.fast_polling().run(Stopwatch)
}

This example demonstrates:

  • State management with the #[update] method handling timer ticks
  • Async effects with the #[effect] method for continuous updates
  • Rich text formatting with inline styles and hex colors
  • Global keyboard event handling with @key and @char_global
  • Layout control with centering and responsive width (w_frac: 1.0)
• • •

The node! Macro

The node! macro is how you actually build your UI. It gives you a clean, declarative syntax that lives inside your component's view method. Instead of imperatively creating and configuring widgets, you describe what the UI should look like:

Basic Syntax

node! {
    // Root node
    div(...<properties>, ...<handlers>) [

        // Children nodes here
        text("content", ...<properties>),
        div(...) [

            // Nested nodes
            ...<children>
        ]
    ]
}

Example:

node! {
    div(
        bg: blue,
        pad: 2,
        border: white,
        @key(enter): ctx.handler("submit"),
        @click: ctx.handler("clicked")
    ) [
        richtext(align: center, wrap: word) [
            text("Welcome to ", color: bright_white),
            text("RxTUI", color: yellow, bold),
            text("!", color: bright_white)
        ],
        div [
            text("Nested content")
        ]
    ]
}

Elements

Expressions

You can use any Rust expression that returns a Node by wrapping it in parentheses:

node! {
    div [
        // Variable
        (my_node_variable),

        // Match expression
        (match state.status {
            Loading => node! { text("Loading...") },
            Ready => node! { text("Ready!") },
        }),

        // If expression
        (if condition {
            node! { text("True branch") }
        } else {
            node! { text("False branch") }
        }),

        // Method call
        (self.create_node()),
    ]
}
Spread Operator

Use the ... spread operator to expand a Vec<Node> as children:

node! {
    div [
        // Spread a vector of nodes
        ...(vec![
            node! { text("Item 1") },
            node! { text("Item 2") },
            node! { text("Item 3") },
        ]),

        // Spread from iterator
        ...(state.items.iter().map(|item| {
            node! {
                div(pad: 1) [
                    text(&item.name)
                ]
            }
        }).collect::<Vec<Node>>()),

        // Combine with regular children
        text("Header", bold),
        ...(item_nodes),
        text("Footer"),
    ]
}

This is particularly useful for rendering lists or collections dynamically.

Div Container
node! {
    div(
        // Layout
        dir: vertical,      // or horizontal, v, h
        gap: 2,            // space between children
        wrap: wrap,        // wrap mode

        // Sizing
        w: 50,             // fixed width
        h: 20,             // fixed height
        w_frac: 0.5,        // 50% of parent width
        h_frac: 0.8,        // 80% of parent height
        w_auto,            // automatic width
        h_content,         // size to content

        // Styling
        bg: blue,          // background color
        pad: 2,            // padding all sides
        pad_h: 1,          // horizontal padding
        pad_v: 1,          // vertical padding

        // Borders
        border: white,     // border color
        border_style: rounded,
        border_color: yellow,
        border_edges: BorderEdges::TOP | BorderEdges::BOTTOM,

        // Interaction
        focusable,         // can receive focus
        overflow: scroll,  // scroll, hidden, auto
        show_scrollbar: true,

        // Positioning
        absolute,          // absolute positioning
        top: 5,
        left: 10,
        z: 100            // z-index
    ) [
        // Children here
    ]
}
Text
node! {
    div [
        // Simple text
        text("Hello"),

        // Styled text
        text("Styled", color: red, bold, italic, underline),

        // Dynamic text
        text(format!("Count: {}", count)),

        // Text with wrapping
        text("Long text...", wrap: word),

        // Text with alignment
        text("Centered", align: center),
        text("Right aligned", align: right)
    ]
}
Rich Text
node! {
    div [
        richtext [
            text("Normal "),
            text("Bold", bold),
            text(" and "),
            text("Colored", color: red)
        ],

        // With top-level styling
        richtext(wrap: word) [
            text("Line 1 "),
            text("Important", color: yellow, bold),
            text(" continues...")
        ],

        // With alignment
        richtext(align: center) [
            text("Centered "),
            text("rich text", bold)
        ]
    ]
}
Stacks
node! {
    div [
        // Vertical stack (default)
        vstack [
            text("Top"),
            text("Bottom")
        ],

        // Horizontal stack
        hstack(gap: 2) [
            text("Left"),
            text("Right")
        ]
    ]
}
Components
node! {
    div [
        // Embed other components
        node(MyComponent::new("config")),
        node(Counter)
    ]
}
Spacers
node! {
    div [
        text("Top"),
        spacer(2),  // 2 lines of space
        text("Bottom")
    ]
}

Event Handlers

node! {
    div(
        focusable,
        // Mouse events
        @click: ctx.handler(Msg::Clicked),
        // Keyboard events (requires focus)
        @char('a'): ctx.handler(Msg::KeyA),
        @key(enter): ctx.handler(Msg::Enter),
        @key(Char('-')): ctx.handler(Msg::Minus),
        // Focus events
        @focus: ctx.handler(Msg::Focused),
        @blur: ctx.handler(Msg::Blurred),
        // Global events (work without focus)
        @char_global('q'): ctx.handler(Msg::Quit),
        @key_global(esc): ctx.handler(Msg::Exit),
        // Any character handler
        @any_char: |ch| ctx.handler(Msg::Typed(ch))
    ) [
        text("Interactive")
    ]
}

Optional Properties

Use ! suffix for optional properties:

node! {
    div(
        // Only applied if Some
        bg: (optional_color)!,
        w: (optional_width)!,
        border: (if selected { Some(Color::Yellow) } else { None })!
    ) [
        text("Conditional styling")
    ]
}
• • •

State Management

These are the heart of your component's logic. State is just your data - what your component needs to remember.

Component State

#[derive(Debug, Clone, Default)]
struct MyState {
    counter: i32,
    text: String,
}

impl MyComponent {
    #[update]
    fn update(&self, ctx: &Context, msg: MyMsg, mut state: MyState) -> Action {
        // The #[update] macro automatically fetches state
        // and passes it as the last parameter

        state.counter += 1;
        Action::update(state)  // Save the new state
    }

    #[view]
    fn view(&self, ctx: &Context, state: MyState) -> Node {
        // The #[view] macro automatically fetches state
        node! {
            div [
                text(format!("Counter: {}", state.counter))
            ]
        }
    }
}

Manual State Access

fn update(&self, ctx: &Context, msg: Box<dyn Message>, _topic: Option<&str>) -> Action {
    // Manually get state (or initialize with Default)
    let mut state = ctx.get_state::<MyState>();

    // Modify state
    state.counter += 1;

    // Return updated state
    Action::update(state)
}
• • •

Message Handling

Messages are how components respond to events - user clicks, key presses, timers firing. When a message arrives, you update your state, and the UI automatically re-renders.

Basic Messages

#[derive(Debug, Clone)]
enum MyMsg {
    Click,
    KeyPress(char),
    Update(String),
}

impl MyComponent {
    #[update]
    fn update(&self, ctx: &Context, msg: MyMsg, mut state: MyState) -> Action {
        match msg {
            MyMsg::Click => {
                state.clicked = true;
                Action::update(state)
            }
            MyMsg::KeyPress(ch) => {
                state.text.push(ch);
                Action::update(state)
            }
            MyMsg::Update(text) => {
                state.text = text;
                Action::update(state)
            }
        }
    }
}

Actions

Update methods return an Action:

pub enum Action {
    Update(Box<dyn State>),              // Update component state
    UpdateTopic(String, Box<dyn State>), // Update topic state
    None,                                // No action
    Exit,                                // Exit application
}

Message with Value

// In view
node! {
    div [
        @any_char: ctx.handler_with_value(|ch| Box::new(MyMsg::Typed(ch)))
    ]
}
• • •

Topic-Based Communication

Topics enable cross-component communication without direct references.

Sending to Topics

impl Dashboard {
    #[update]
    fn update(&self, ctx: &Context, msg: DashboardMsg, state: DashboardState) -> Action {
        match msg {
            DashboardMsg::NotifyAll => {
                // Send message to topic
                ctx.send_to_topic("notifications", NotificationMsg::Alert);
                Action::none()
            }
        }
    }
}

Receiving Topic Messages

impl NotificationBar {
    // Static topic
    #[update(msg = LocalMsg, topics = ["notifications" => NotificationMsg])]
    fn update(&self, ctx: &Context, messages: Messages, mut state: State) -> Action {
        match messages {
            Messages::LocalMsg(msg) => {
                // Handle local messages
            }
            Messages::NotificationMsg(msg) => {
                // Handle topic messages
                // Returning Action::update claims topic ownership
                state.notifications.push(msg);
                Action::update(state)
            }
        }
    }
}

Dynamic Topics

struct Counter {
    topic_name: String,  // Topic determined at runtime
}

impl Counter {
    // Dynamic topic from field
    #[update(msg = CounterMsg, topics = [self.topic_name => ResetSignal])]
    fn update(&self, ctx: &Context, messages: Messages, mut state: CounterState) -> Action {
        match messages {
            Messages::CounterMsg(msg) => { /* ... */ }
            Messages::ResetSignal(_) => {
                // Reset when signal received
                Action::update(CounterState::default())
            }
        }
    }
}

Topic State

// Write topic state (first writer becomes owner)
Action::UpdateTopic("app.settings".to_string(), Box::new(settings))

// Read topic state from any component
let settings: Option<Settings> = ctx.read_topic("app.settings");
• • •

Layout System

RxTUI provides a flexible layout system with multiple sizing modes.

Dimension Types

pub enum Dimension {
    Fixed(u16),       // Exact size in cells
    Percentage(f32),  // Percentage of parent (stored 0.0 to 1.0)
    Auto,            // Share remaining space equally
    Content,         // Size based on children
}

Layout Examples

node! {
    // Fixed layout
    div(w: 80, h: 24) [
        text("Fixed size")
    ],

    // Percentage-based
    div(w_frac: 0.5, h_frac: 0.8) [
        text("50% width, 80% height")
    ],

    // Auto sizing - share remaining space
    hstack [
        div(w: 20) [ text("Fixed") ],
        div(w_auto) [ text("Auto 1") ],  // Gets 50% of remaining
        div(w_auto) [ text("Auto 2") ]   // Gets 50% of remaining
    ],

    // Content-based sizing
    div(w_content, h_content) [
        text("Size fits content")
    ]
}

Direction and Wrapping

node! {
    // Vertical layout (default)
    div(dir: vertical, gap: 2) [
        text("Line 1"),
        text("Line 2")
    ],

    // Horizontal layout
    div(dir: horizontal, gap: 1) [
        text("Col 1"),
        text("Col 2")
    ],

    // With wrapping
    div(dir: horizontal, wrap: wrap, w: 40) [
        // Children wrap to next line when width exceeded
        div(w: 15) [ text("Item 1") ],
        div(w: 15) [ text("Item 2") ],
        div(w: 15) [ text("Item 3") ]  // Wraps to next line
    ]
}

Scrolling

node! {
    div(
        h: 10,              // Fixed container height
        overflow: scroll,   // Enable scrolling
        show_scrollbar: true,
        focusable          // Must be focusable for keyboard scrolling
    ) [
        // Content taller than container
        text("Line 1"),
        text("Line 2"),
        // ... many more lines
        text("Line 50")
    ]
}

Scrolling controls:

  • Arrow keys: Scroll up/down by 1 line
  • Page Up/Down: Scroll by container height
  • Home/End: Jump to top/bottom
  • Mouse wheel: Scroll up/down

Note: Only vertical scrolling is currently implemented.

• • •

Styling

Colors

RxTUI supports multiple color formats:

node! {
    div [
        // Named colors
        text("Red", color: red),
        text("Bright Blue", color: bright_blue),

        // Hex colors
        text("Hex", color: "#FF5733"),

        // RGB
        text("RGB", color: (Color::Rgb(255, 128, 0))),

        // Conditional
        text("Status", color: (if ok { Color::Green } else { Color::Red }))
    ]
}

Available named colors:

  • Basic: black, red, green, yellow, blue, magenta, cyan, white
  • Bright: bright_black, bright_red, bright_green, bright_yellow, bright_blue, bright_magenta, bright_cyan, bright_white

Text Alignment

Text and RichText nodes support horizontal alignment within their containers:

node! {
    div(w: 50) [
        // Basic text alignment
        text("Left aligned", align: left),
        text("Centered text", align: center),
        text("Right aligned", align: right),

        // RichText alignment
        richtext(align: center) [
            text("This "),
            text("rich text", bold),
            text(" is centered")
        ],

        // Alignment with wrapping
        text(
            "Long text that wraps to multiple lines. Each line will be aligned.",
            wrap: word,
            align: right
        )
    ]
}

Note: Text nodes with alignment automatically expand to fill their parent's width to enable proper alignment calculation.

Div Alignment (Flexbox-style)

Divs support CSS Flexbox-style alignment for their children along both the main and cross axes:

node! {
    // Justify content (main axis)
    div(dir: h, justify: center, w: 50) [
        div(w: 10, h: 3, bg: red) [],
        div(w: 10, h: 3, bg: green) [],
        div(w: 10, h: 3, bg: blue) []
    ],

    // Align items (cross axis)
    div(dir: h, align: end, w: 50, h: 10) [
        div(w: 10, h: 3, bg: red) [],
        div(w: 10, h: 5, bg: green) [],
        div(w: 10, h: 7, bg: blue) []
    ],

    // Combined justify and align
    div(dir: v, justify: space_between, align: center, w: 40, h: 20) [
        text("Item 1"),
        text("Item 2"),
        text("Item 3")
    ],

    // With align_self override
    div(dir: h, align: start, w: 50, h: 10) [
        div(w: 10, h: 3, bg: red) [],
        div(w: 10, h: 3, bg: green, align_self: center) [],
        div(w: 10, h: 3, bg: blue, align_self: end) []
    ]
}

JustifyContent (distributes items along main axis):

  • start - Pack items at the start (default)
  • center - Center items
  • end - Pack items at the end
  • space_between - Distribute evenly, first at start, last at end
  • space_around - Equal space around each item
  • space_evenly - Equal space between and around items

AlignItems (aligns items on cross axis):

  • start - Align at the start (default)
  • center - Center items
  • end - Align at the end

AlignSelf (per-child cross axis override):

  • auto - Use parent's align_items (default)
  • start - Align at the start
  • center - Center
  • end - Align at the end

The main axis is determined by the direction:

  • dir: h (horizontal) - main axis is horizontal, cross axis is vertical
  • dir: v (vertical) - main axis is vertical, cross axis is horizontal

Borders

node! {
    div [
        // Simple border
        div(border: white) [ text("Single border") ],

        // Border styles
        div(
            border_style: rounded,
            border_color: cyan
        ) [
            text("Rounded border")
        ],

        // Partial borders
        div(
            border: white,
            border_edges: top | bottom
        ) [
            text("Top and bottom only")
        ]
    ]
}

Border styles:

  • Single - Normal lines
  • Double - Double lines
  • Rounded - Rounded corners
  • Thick - Thick lines

Spacing

node! {
    div [
        // Padding
        div(pad: 2) [ text("All sides") ],
        div(pad_h: 2) [ text("Horizontal") ],
        div(pad_v: 1) [ text("Vertical") ],
        div(padding: (Spacing::new(1, 2, 3, 4))) [ text("Custom") ],

        // Gap between children
        div(gap: 2) [
            text("Item 1"),
            text("Item 2")  // 2 cells gap
        ]
    ]
}

Focus Styles

node! {
    div(
        focusable,
        border: white,
        focus_style: ({
            Style::default()
                .background(Color::Blue)
                .border(Color::Yellow)
        })
    ) [
        text("Changes style when focused")
    ]
}
• • •

Event Handling

Focus-Based Events

Most events require the element to be focused:

node! {
    div(
        focusable,

        // Mouse
        @click: ctx.handler(Msg::Clicked),

        // Keyboard
        @char('a'): ctx.handler(Msg::PressedA),
        @key(enter): ctx.handler(Msg::Confirmed),
        @key(backspace): ctx.handler(Msg::Delete),

        // Focus
        @focus: ctx.handler(Msg::GainedFocus),
        @blur: ctx.handler(Msg::LostFocus)
    ) [
        text("Click or press keys")
    ]
}

Global Events

Global events work regardless of focus:

node! {
    div(
        // Application-wide shortcuts
        @char_global('q'): ctx.handler(Msg::Quit),
        @key_global(esc): ctx.handler(Msg::Cancel),
        @char_global('/'): ctx.handler(Msg::Search)
    ) [
        // Children here
    ]
}

Focus Navigation

  • Tab: Move to next focusable element
  • Shift+Tab: Move to previous focusable element

Programmatic Focus

Use the Context focus helpers to move focus immediately after a render:

#[view]
fn view(&self, ctx: &Context, state: MyState) -> Node {
    if ctx.is_first_render() {
        ctx.focus_self(); // focus the first focusable node in this component
    }

    node! {
        div [
            input(focusable),
            button(focusable)
        ]
    }
}
  • ctx.focus_self() focuses the first focusable element inside the component's subtree.
  • ctx.focus_first() focuses the first focusable element in the entire app.
  • ctx.is_first_render() is handy for gating autofocus so you do not wrestle with user-driven focus changes later.
• • •

Built-in Components

TextInput

A full-featured text input component:

use rxtui::components::TextInput;

node! {
    div [
        // Basic input
        input(placeholder: "Enter name...", focusable),

        // Custom styling
        input(
            placeholder: "Password...",
            password,              // Mask input
            border: yellow,
            w: 40,
            content_color: green,
            cursor_color: white
        ),

        // Or use the builder API
        node(
            TextInput::new()
                .placeholder("Email...")
                .width(50)
                .border(Color::Cyan)
                .focus_border(Color::Yellow)
        )
    ]
}

TextInput features:

  • Full text editing (insert, delete, backspace)
  • Cursor movement (arrows, Home/End)
  • Word navigation (Alt+B/F or Ctrl+arrows)
  • Word deletion (Ctrl+W, Alt+D)
  • Line deletion (Ctrl+U/K)
  • Password mode
  • Placeholder text
  • Customizable styling
• • •

Effects (Async)

Effects enable async operations like timers, network requests, and file monitoring.

Basic Effect

use rxtui::prelude::*;
use std::time::Duration;

#[derive(Component)]
struct Timer;

#[component]  // Required to collect #[effect] methods
impl Timer {
    #[update]
    fn update(&self, ctx: &Context, msg: TimerMsg, mut state: TimerState) -> Action {
        match msg {
            TimerMsg::Tick => {
                state.seconds += 1;
                Action::update(state)
            }
        }
    }

    #[view]
    fn view(&self, ctx: &Context, state: TimerState) -> Node {
        node! {
            div [
                text(format!("Time: {}s", state.seconds))
            ]
        }
    }

    #[effect]
    async fn tick(&self, ctx: &Context) {
        loop {
            tokio::time::sleep(Duration::from_secs(1)).await;
            ctx.send(TimerMsg::Tick);
        }
    }
}

Multiple Effects

#[component]
impl MyComponent {
    #[effect]
    async fn monitor_file(&self, ctx: &Context) {
        // Watch for file changes
    }

    #[effect]
    async fn fetch_data(&self, ctx: &Context, state: MyState) {
        // Effects can access state
        if state.should_fetch {
            // Fetch from API
        }
    }
}

Manual Effects

impl Component for MyComponent {
    fn effects(&self, ctx: &Context) -> Vec<Effect> {
        vec![
            Box::pin(async move {
                // Async code
            })
        ]
    }
}
• • •

Advanced Topics

Performance Tips

  1. Use keys for lists: Helps with efficient diffing (not yet implemented)
  2. Minimize state updates: Only update when necessary
  3. Use topics wisely: Don't overuse for simple parent-child communication
  4. Profile rendering: Use RenderConfig for debugging

Debugging

let mut app = App::new()?
    .render_config(RenderConfig {
        use_double_buffer: false,  // Disable for debugging
        use_diffing: false,        // Show all updates
        poll_duration_ms: 100,     // Slow down for observation
    });
app.run(MyComponent)?;