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 anArc<Mutex<T>>internally, allowing safe shared ownership and mutation across threads. When you calluse_state(value), you get aState<T>.count.get(): This method returns anInner<'a, T>guard.InnerimplementsDerefandDerefMut, so you can treat it almost like a direct reference to your data (*count.get()). TheInnerguard 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 useget_mut()orset(). Useful if you modify the internalArc<Mutex<T>>directly (not recommended) or a complex part ofTwithout triggering theDerefMutauto-update.
Important Considerations for State<T>:
- Cloning
Statehandles:State<T>itself can be cloned (count.clone()). This creates a newArcreference to the same underlying state. This is essential when movingStateinto closures or child components. Send + Sync: The typeTheld byState<T>must implementSendandSyncfor thread-safe access.- Reactivity:
use_statemakes state reactive. When the value changes, anyuse_effectorrsx!dynamic scope (%state @if...or%state @for...) that declared thisStateas 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:
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.dependencies: &[&dyn HookDependency]: A slice of references to objects that implement theHookDependencytrait. These are typicallyState<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
Stateobjects as dependencies, the effect will run:- Once, immediately when
use_effectis called (during the initial component render). - Again, whenever any of the listed
Stateobjects trigger an update.
- Once, immediately when
HookDependencyTrait: This trait defines how an object can register anHookEffectto be called upon update.State<T>andMountboth 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 aMountinstance that automatically callsmount()immediately upon its creation. Effects registered with thisMountinstance will run once when the component is first rendered.use_mount_manual(): Returns aMountinstance that starts in an unmounted state. Effects registered with it will only run when you explicitly callmount_instance.mount(). This is useful for controlling the mount event from specificrsx!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.