Skip to main content
Version: 0.2.0

State Management

Effective state management is crucial for building interactive and dynamic TUI applications. OSUI provides a React-like hook system that enables components to hold mutable state and react to changes efficiently. This guide covers the core state management hooks: use_state and use_effect.

use_state: Managing Component-Local State

The use_state hook allows your components to declare and manage mutable, reactive state. When state managed by use_state changes, OSUI automatically re-renders affected parts of your UI.

Basic Usage

use osui::prelude::*;
use std::sync::Arc;

#[component]
fn Counter(cx: &Arc<Context>) -> View {
// 1. Initialize state with `use_state`
// `count` is a `State<i32>`, initialized to 0.
let count = use_state(0);

// 2. Define a button that increments the count
// This is a placeholder for actual interactive elements.
// In a real app, an event handler would trigger `count.set()` or `*count.get_mut()`.
let increment_button_simulated = {
let count = count.clone(); // Clone the State handle to move into the closure
move || {
// Option 1: Using `set()` for direct replacement
// count.set(*count.get() + 1);

// Option 2: Using `get()` for mutable access (recommended for complex changes)
*count.get() += 1; // `Inner` guard automatically calls `update()` on drop
}
};

// Simulate clicking the button once per render for demonstration
// In a real app, this would be triggered by user input or other events.
increment_button_simulated();

rsx! {
// 3. Display the state value
// `count.get_dl()` gets a cloned, deadlock-less copy of the value.
// `count.get()` returns a guard for mutable access, also deadlock-less.
format!("Count: {}", count.get_dl())
}.view(&cx)
}

pub fn main() {
let engine = Console::new();
engine.run(Counter {}).expect("Failed to run Counter app");
}

In this example, the Counter component's state (count) is incremented on each render cycle (simulated). The rsx! macro automatically updates to reflect the new count value because the Counter component is re-rendered by the engine and the count state is used.

State<T> and Inner<'a, T>

  • State<T>: This is the primary handle to your reactive state. It's an Arc<Mutex<T>> internally, allowing safe shared ownership and mutation across threads. When you call use_state(value), you get a State<T>.
  • count.get(): This method returns an Inner<'a, T> guard. Inner implements Deref and DerefMut, so you can treat it almost like a direct reference to your data (*count.get()). The Inner guard is crucial because it automatically triggers updates to dependents when it's dropped if the value was mutated.
  • count.set(value): A convenience method to replace the entire state value and then trigger updates.
  • count.get_dl(): Returns a cloned copy of the state value. The "dl" stands for "deadlock-less", as it doesn't hold the mutex lock for an extended period, making it safer for quick reads. Use this when you only need to read the value and cloning is cheap.
  • count.update(): Manually notifies all dependents that the state might have changed, even if you didn't use get_mut() or set(). Useful if you modify the internal Arc<Mutex<T>> directly (not recommended) or a complex part of T without triggering the DerefMut auto-update.

Important Considerations for State<T>:

  • Cloning State handles: State<T> itself can be cloned (count.clone()). This creates a new Arc reference to the same underlying state. This is essential when moving State into closures or child components.
  • Send + Sync: The type T held by State<T> must implement Send and Sync for thread-safe access.
  • Reactivity: use_state makes state reactive. When the value changes, any use_effect or rsx! dynamic scope (%state @if... or %state @for...) that declared this State as a dependency will be re-evaluated.

use_effect: Performing Side Effects

The use_effect hook allows you to perform side effects (e.g., logging, network requests, setting up event listeners) in response to state changes or component mounting.

Basic Usage

use osui::prelude::*;
use std::sync::Arc;

#[component]
fn EffectExample(cx: &Arc<Context>) -> View {
let count = use_state(0);
let message = use_state("Initial message".to_string());

// Effect 1: Logs when `count` changes
use_effect(
{
let count = count.clone(); // Clone for the closure
move || {
println!("Effect 1: Count changed to {}", count.get_dl());
}
},
&[&count], // Dependencies: &[&dyn HookDependency]
);

// Effect 2: Logs when `message` changes (and also on initial render)
use_effect(
{
let message = message.clone();
move || {
println!("Effect 2: Message is now '{}'", message.get_dl());
}
},
&[&message], // Dependencies: &[&dyn HookDependency]
);

// Simulate state changes (e.g., from user input or timers)
let _ = {
let count = count.clone();
let message = message.clone();
std::thread::spawn(move || {
sleep(500); // Wait 0.5 seconds
*count.get() += 1; // Triggers Effect 1
sleep(500);
message.set("Updated message!".to_string()); // Triggers Effect 2
sleep(500);
*count.get() += 1; // Triggers Effect 1 again
});
};

rsx! {
format!("Count: {}", count.get_dl())
format!("Message: {}", message.get_dl())
}.view(&cx)
}

pub fn main() {
let engine = Console::new();
engine.run(EffectExample {}).expect("Failed to run EffectExample app");
}

use_effect Parameters:

  1. f: F: A closure (FnMut() + Send + Sync + 'static) that represents the side effect. This closure will be executed when any of its dependencies change. The closure is run in a separate spawned thread to avoid blocking the main rendering loop.
  2. dependencies: &[&dyn HookDependency]: A slice of references to objects that implement the HookDependency trait. These are typically State<T> instances. The effect closure will be called whenever any of these dependencies notify an update.

Understanding Dependencies:

  • Empty Dependency List (&[]): If you pass an empty slice (&[]), the effect will only run once when the component is first mounted. This is useful for setup logic like initializing global resources or subscriptions.
  • Specific Dependencies: When you list State objects as dependencies, the effect will run:
    • Once, immediately when use_effect is called (during the initial component render).
    • Again, whenever any of the listed State objects trigger an update.
  • HookDependency Trait: This trait defines how an object can register an HookEffect to be called upon update. State<T> and Mount both implement this trait, making them usable as dependencies.

Lifecycle Hooks: use_mount and use_mount_manual

These hooks are special cases of use_effect for managing actions tied to a component's "mounting" lifecycle.

  • use_mount(): Returns a Mount instance that automatically calls mount() immediately upon its creation. Effects registered with this Mount instance will run once when the component is first rendered.
  • use_mount_manual(): Returns a Mount instance that starts in an unmounted state. Effects registered with it will only run when you explicitly call mount_instance.mount(). This is useful for controlling the mount event from specific rsx! nodes (!mount_instance) or from other logic.
use osui::prelude::*;
use std::sync::Arc;

#[component]
fn LifecycleExample(cx: &Arc<Context>) -> View {
let mount_hook = use_mount(); // Automatically mounted
let manual_mount_hook = use_mount_manual(); // Manual mount needed

use_effect(
move || {
println!("Component mounted (automatic hook)!");
},
&[&mount_hook],
);

use_effect(
move || {
println!("Component manually mounted!");
},
&[&manual_mount_hook],
);

rsx! {
"Lifecycle example"
// This will trigger the `manual_mount_hook`'s effects
!manual_mount_hook
}.view(&cx)
}

pub fn main() {
let engine = Console::new();
engine.run(LifecycleExample {}).expect("Failed to run LifecycleExample app");
}

This guide has laid the foundation for managing state and side effects in your OSUI applications. By mastering use_state and use_effect, you can build sophisticated and responsive TUI experiences.

Next: Learn how to handle user interactions and other events with OSUI's Event Handling system.