| Crates.io | nami |
| lib.rs | nami |
| version | 0.6.1 |
| created_at | 2025-08-30 10:31:33.910741+00 |
| updated_at | 2025-09-11 14:51:54.274755+00 |
| description | A powerful, lightweight reactive framework. |
| homepage | https://github.com/water-rs/nami |
| repository | https://github.com/water-rs/nami |
| max_upload_size | |
| id | 1817509 |
| size | 151,095 |
A powerful, lightweight reactive framework for Rust.
no_std with allocSignal trait across computed valuesBinding<T> with helpersmap, zip, cached, debounce, throttle, utils::{add,max,min}use nami::{binding, Binding, Signal};
// Create mutable reactive state with automatic type conversion
let counter: Binding<i32> = binding(0);
let message: Binding<String> = binding("hello"); // &str -> String conversion
// Derive a new computation from it
let doubled = nami::map::map(counter.clone(), |n: i32| n * 2);
// Read current values
assert_eq!(counter.get(), 0);
assert_eq!(doubled.get(), 0);
// Update the source and observe derived changes
counter.set(3);
assert_eq!(doubled.get(), 6);
// set() also accepts Into<T> for ergonomic updates
message.set("world"); // &str works directly!
Signal TraitAll reactive values implement a single trait:
use nami::watcher::{Context, WatcherGuard};
pub trait Signal: Clone + 'static {
type Output;
fn get(&self) -> Self::Output;
fn watch(&self, watcher: impl Fn(Context<Self::Output>) + 'static) -> impl WatcherGuard;
}
get: compute and return the current value.watch: subscribe to changes; returns a guard. Drop the guard to unsubscribe.Binding, Computed, and all adapters implement Signal so you can compose them freely.
Binding<T> is two-way reactive state with ergonomic helpers. Both binding() and set() accept any value implementing Into<T>, eliminating the need for manual conversions:
use nami::{binding, Binding};
// Automatic type conversion with Into trait
let text: Binding<String> = binding("hello"); // &str -> String
let counter: Binding<i32> = binding(0); // Direct initialization
let items: Binding<Vec<i32>> = binding(vec![1, 2, 3]); // Vec<i32> binding
// set() also uses Into<T> for ergonomic updates
text.set("world"); // Direct &str, no .into() needed
counter.set(5);
counter.increment(1);
assert_eq!(counter.get(), 6);
// Works with type conversions
let bignum: Binding<i64> = binding(0i64);
bignum.set(42i32); // i32 -> i64 automatic conversion
Common helpers:
Binding<bool>: toggle(), then(...), select(a,b)Binding<i32>: increment(n), decrement(n)Binding<String>: append(...), clear()Binding<Vec<T>>: push(...), insert(...), pop(), clear()React to changes via watch. Keep the returned guard alive to stay subscribed.
use nami::{binding, Binding, Signal, watcher::Context};
let name: Binding<String> = binding("World");
let _guard = name.watch(|ctx: Context<String>| {
// metadata is available via ctx.metadata
// println! is just an example side-effect
println!("Hello, {}!", ctx.value);
});
name.set("Universe");
The Context carries typed metadata to power advanced features (e.g., animations).
map(source, f): transform values while preserving reactivityzip(a, b): combine two signals into (A::Output, B::Output)cached(signal): cache last value and avoid recomputationdebounce(signal, duration): delay updates until a quiet periodthrottle(signal, duration): limit update rate to at most once per durationutils::{add, max, min}: convenient combinators built on zip + mapuse nami::{binding, Binding, Signal};
use nami::{map::map, zip::zip};
let a: Binding<i32> = binding(2);
let b: Binding<i32> = binding(3);
let sum = nami::utils::add(a.clone(), b.clone());
assert_eq!(sum.get(), 5);
let pair = zip(a, b);
assert_eq!(pair.get(), (2, 3));
let squared = map(sum, |n: i32| n * n);
assert_eq!(squared.get(), 25);
Control the rate of updates with debounce and throttle utilities:
use nami::{binding, debounce::Debounce, throttle::Throttle, Binding};
use core::time::Duration;
let input: Binding<String> = binding("");
// Debounce: delay updates until 300ms of quiet time
let debounced = Debounce::new(input.clone(), Duration::from_millis(300));
// Throttle: limit to at most one update per 100ms
let throttled = Throttle::new(input.clone(), Duration::from_millis(100));
// Both preserve reactivity while controlling update frequency
input.set("typing...");
Debounce vs Throttle:
Computed<T>Computed<T> stores any Signal<Output = T> behind a stable, type-erased handle.
use nami::{Signal, SignalExt};
let c = 10_i32.computed();
assert_eq!(c.get(), 10);
let plus_one = c.map(|n| n + 1);
assert_eq!(plus_one.get(), 11);
Bridge async with reactive using adapters:
FutureSignal<T>: Option<T> becomes Some(T) when a future resolvesSignalStream<S>: treat a Signal as a Stream that yields on updatesBindingMailbox<T>: cross-thread reactive state with get(), set(), and get_as() for type conversionuse nami::future::FutureSignal;
use executor_core::LocalExecutor;
// Requires an executor; example omitted for brevity
// let sig = FutureSignal::new(executor, async { 42 });
// assert_eq!(sig.get(), None);
// ... later ... sig.get() == Some(42)
use nami::{Signal, stream::SignalStream};
// let s = /* some Signal */;
// let mut stream = SignalStream { signal: s, guard: None };
// while let Some(value) = stream.next().await { /* ... */ }
Enhanced Mailboxes (requires native-executor feature):
use nami::{binding, Binding};
use waterui_str::Str; // Example non-Send type
// Create binding with non-Send type
let text_binding:Binding<Str> = binding("hello");
let mailbox = text_binding.mailbox();
// Convert to Send type for cross-thread usage
let owned_string: String = mailbox.get_as().await;
assert_eq!(owned_string, "hello");
// Regular mailbox operations
mailbox.set("world").await;
Enable structured logging to trace signal behavior during development:
use nami::{binding, Binding, Signal, debug::{Debug, Config}};
let value: Binding<i32> = binding(42);
// Log only value changes (most common)
let debug = Debug::changes(value.clone());
// Log all operations (verbose mode)
let debug = Debug::verbose(value.clone());
// Log specific operations
let debug = Debug::compute_only(value.clone()); // Only computations
let debug = Debug::watchers(value.clone()); // Watcher lifecycle
let debug = Debug::compute_and_changes(value.clone()); // Both computations and changes
// Use custom configuration
let debug = Debug::with_config(value, Config::default());
The debug module uses the log crate for output, so configure your logger (e.g., env_logger) to see the debug messages.
Enable the derive feature (enabled by default) to access:
#[derive(nami::Project)]: project a struct binding into bindings for each fielduse nami::{binding, Binding, project::Project};
#[derive(Clone, nami::Project)]
struct Person { name: String, age: u32 }
let p: Binding<Person> = binding(Person { name: "A".into(), age: 1 });
// The derive generates `PersonProjected`
let projected: PersonProjected = p.project();
projected.name.set("B"); // Automatic &str -> String conversion
assert_eq!(p.get().name, "B");
Feature flags:
derive (default): re-exports macros from nami-derivenative-executor (default): integrates with native-executor for mailbox helpersno_std: the crate is #![no_std] and uses alloc.no_run because they require an executor or side effects.