Skip to main content
Version: 0.1.1

State and Reactivity in OSUI

OSUI's reactivity model enables your UI to automatically update when underlying data changes, eliminating the need for manual DOM manipulation. This is achieved through the State<T> struct and its integration with DynWidgets via the DependencyHandler trait.

The Problem: Dynamic UIs

Imagine you have a counter that increments over time, and you want to display its current value in your TUI. Without a reactive system, you would manually:

  1. Get the new count.
  2. Clear the old count from the screen.
  3. Draw the new count at the correct position.
  4. Manage redraws if other elements shift.

This becomes complex quickly, especially with multiple, interconnected pieces of dynamic data.

The OSUI Solution: State<T>

State<T> is a generic type that wraps your data T and provides mechanisms to signal when T has changed. This signal then triggers a re-render of any UI widget that depends on that State.

1. Creating Reactive State: use_state()

The use_state function is the primary way to create a new State<T> instance:

use osui::prelude::*;

fn main() -> std::io::Result<()> {
let screen = Screen::new();
screen.extension(InputExtension); // Needed for event loop

// Create a new State<i32> initialized with 0
let counter = use_state(0);

// ... UI definition and screen.run()
Ok(())
}

State<T> internally uses Arc<Mutex<Inner<T>>>, making it Send + Sync and safely shareable across threads and widgets.

2. Modifying State and Triggering Updates

The most common way to interact with State<T> is via its get() method, which returns a MutexGuard<'_, Inner<T>>. The Inner<T> struct implements Deref and DerefMut for T, allowing you to treat state.get() much like a direct mutable reference to your data.

Key Point: When you use DerefMut (e.g., *state.get() = ... or *state.get() += 1), the State automatically registers that it has changed. This is critical for reactivity.

use osui::prelude::*;
use std::{thread, time::Duration};

fn main() -> std::io::Result<()> {
let screen = Screen::new();
screen.extension(InputExtension); // Required for terminal setup

let counter = use_state(0);

// Spawn a new thread to increment the counter every second.
// We clone `counter` (which is cheap due to Arc) to move it into the thread.
thread::spawn({
let counter_clone = counter.clone(); // Clone the Arc for the new thread
move || {
loop {
// Get a mutable lock on the counter state
// The DerefMut implementation on `Inner<i32>` will mark the state as changed
// (setting `inner.changed = inner.dependencies`)
*counter_clone.get() += 1;
thread::sleep(Duration::from_secs(1));
}
}
});

rsx! {
// Declare that this Div widget depends on the `counter` state.
// The `%counter` syntax is a critical part of connecting state to UI.
%counter
Div {
// Access the value of the state using `.get()`.
// `format!` macro will automatically call `Display` for `State<T>`.
format!("Current Count: {}", counter.get())
}
}
.draw(&screen);

screen.run()?;
Ok(())
}

In this example:

  • *counter_clone.get() += 1; modifies the i32 value and simultaneously tells State<i32> that it has been updated.
  • Because the Div element was declared with %counter in rsx!, it becomes a DynWidget and registers counter as one of its dependencies.
  • In the main rendering loop (inside screen.run()), DynWidgets repeatedly call dependency.check(). When counter.check() returns true (because it was marked as changed), the DynWidget rebuilds its internal Element and components, refreshing its content with the new counter value.

3. Declaring Dependencies in rsx!

The %variable_name syntax in the rsx! macro is how you declare that a widget depends on a State<T> variable (or any type implementing DependencyHandler).

let my_data = use_state("Initial".to_string());
// ...
rsx! {
%my_data // This Div will re-render when `my_data` changes
Div {
format!("Data: {}", my_data.get())
}
}

You can declare multiple dependencies: %state1 %state2 Div { ... }.

When to use State::set() or State::update()

  • State::set(new_value): Use this when you want to entirely replace the T value within the State and mark it as changed.
    // Instead of: *my_state.get() = "New".to_string();
    my_state.set("New".to_string());
  • State::update(): Use this if you modify the inner T value through a path that doesn't trigger DerefMut (e.g., if T is a complex struct and you modify one of its fields after getting a &mut T but without re-assigning the whole T). This explicitly tells State that it has changed.
    // If MyComplexData has an internal field modified, and you only got `&mut MyComplexData`
    // but didn't reassign the whole struct.
    let mut data_lock = my_complex_data.get();
    data_lock.some_internal_field.modify();
    // After modifying, you might need to manually update if DerefMut didn't catch it
    // (though in most simple cases, DerefMut handles it automatically).
    my_complex_data.update();

Cloning State<T>

Since State<T> uses Arc, cloning a State<T> instance is very cheap. It only increments the reference count of the shared Arc. This is how you pass State into closures or other threads without moving ownership.

let original_state = use_state(0);
let cloned_state = original_state.clone(); // This is just an Arc clone

By leveraging State<T> and dependency tracking, OSUI enables you to build dynamic, responsive terminal UIs with a clear separation of concerns between data and presentation.