Skip to content

Counter tutorial

The counter is the first full tutorial. A number wakes once per second, and that tiny loop walks through Ratatui Kit’s core path: component update, hooks retaining state, an async task writing state, and a waker waking the next frame.

counter demo

This recording is generated by docs/tapes/counter.tape and runs the real example from the repository.

cargo run --example counter

Press q to exit, or use Ctrl+C to force exit.

The full source is in examples/start/counter.rs. Here is the component body:

#[component]
fn Counter(mut hooks: Hooks) -> impl Into<AnyElement<'static>> {
    let mut count = hooks.use_state(|| 0_u64);
    hooks.use_future(async move {
        loop {
            tokio::time::sleep(std::time::Duration::from_secs(1)).await;
            count += 1;
        }
    });

    let mut exit = hooks.use_exit();
    hooks.use_event_handler(EventScope::Current, EventPriority::Normal, move |event| {
        let Event::Key(key) = event else {
            return EventResult::Ignored;
        };
        if key.kind == KeyEventKind::Press
            && matches!(key.code, KeyCode::Char('q') | KeyCode::Char('Q'))
        {
            exit();
            return EventResult::Consumed;
        }

        EventResult::Ignored
    });

    element!(
        Center(
            width: Constraint::Length(48),
            height: Constraint::Length(9),
        ) {
            Border(
                flex_direction: Direction::Vertical,
                justify_content: Flex::Center,
                border_style: Style::new().cyan(),
                top_title: Line::from(" ratatui-kit counter ").cyan().bold().centered(),
                bottom_title: Line::from(" q quit · Ctrl+C exit ").dark_gray().centered(),
            ){
                View(
                    flex_direction: Direction::Vertical,
                    justify_content: Flex::Center,
                    gap: 1,
                ) {
                    View(height: Constraint::Length(1)){
                        Text(text: Line::styled(
                            format!("Counter: {:02}", count.get()),
                            Style::new().green().bold(),
                        ).centered())
                    }
                    View(height: Constraint::Length(1)){
                        Text(text: Line::from("state writes wake the terminal UI").centered())
                    }
                }
            }
        }
    )
}

#[component] turns Counter into a persistent UI unit. The function runs again every frame, but hooks are attached to the component instance and do not disappear when the function returns.

use_state creates local state:

let mut count = hooks.use_state(|| 0_u64);

count is a copyable reactive handle. It is not an ordinary local variable; writing to it wakes the runtime.

use_future starts one async task when the component mounts:

hooks.use_future(async move {
    loop {
        tokio::time::sleep(std::time::Duration::from_secs(1)).await;
        count += 1;
    }
});

Every count += 1 marks the state as changed. The render loop is waiting for either “component tree changed” or “terminal event”, so it wakes, reruns the component function, and draws the new number to the terminal.

element! describes this frame’s UI:

element!(
    Center(width: Constraint::Length(48), height: Constraint::Length(9)) {
        Border {
            Text(text: format!("Counter: {:02}", count.get()))
        }
    }
)

This conceptual snippet omits style and inner layout, keeping only the outline of the element tree. The Element here is a cheap declaration rebuilt every frame; the runtime-reused component instance is what actually stores state.

This example runs through the framework’s smallest closed loop:

  • The entrypoint starts with element!(Counter).fullscreen().await.
  • The component function declares UI.
  • Hooks retain state and side effects across frames.
  • Reactive state wakes the next frame.
  • The Ratatui buffer is written only during draw.

Next, read Async data states to replace “increment on a timer” with “request, loading, error, and data refresh”.