Expand description
§Nami
A powerful, lightweight reactive framework for Rust.
no_stdwithalloc- Consistent
Signaltrait across computed values - Ergonomic two-way
Binding<T>with helpers - Composition primitives:
map,zip,cached,debounce,throttle,utils::{add,max,min} - Typed watcher context with metadata
- Optional derive macros
§Quick Start
use nami::{binding, Binding, Signal};
// Create mutable reactive state with automatic type conversion
let mut counter: Binding<i32> = binding(0);
let mut 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_from() also accepts Into<T> for ergonomic updates
message.set_from("world"); // &str works directly!§The Signal Trait
All reactive values implement a single trait:
use nami::watcher::{Context, WatcherGuard};
pub trait Signal: Clone + 'static {
type Output;
type Guard: WatcherGuard;
fn get(&self) -> Self::Output;
fn watch(&self, watcher: impl Fn(Context<Self::Output>) + 'static) -> Self::Guard;
}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.
§Bindings
Binding<T> is two-way reactive state with ergonomic helpers. Both binding() and set_from() accept any value implementing Into<T>, eliminating the need for manual conversions:
use nami::{binding, Binding};
// Automatic type conversion with Into trait
let mut text: Binding<String> = binding("hello"); // &str -> String
let mut counter: Binding<i32> = binding(0); // Direct initialization
let mut bignum: Binding<i64> = binding(0i64);
let items: Binding<Vec<i32>> = binding(vec![1, 2, 3]); // Vec<i32> binding
// set_from() also uses Into<T> for ergonomic updates
text.set_from("world"); // Direct &str, no .into() needed
counter.set(5);
counter.add_assign(1);
assert_eq!(counter.get(), 6);
// Works with type conversions
let mut bignum: Binding<i64> = binding(0);
bignum.set(42);§Watchers
React to changes via watch. Keep the returned guard alive to stay subscribed.
use nami::{binding, Binding, Signal, watcher::Context};
let mut 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_from("Universe");The Context carries typed metadata to power advanced features (e.g., animations).
§Composition Primitives
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 onzip+map
use 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);§Rate Limiting: Debounce and Throttle
Control the rate of updates with debounce and throttle utilities:
use nami::{binding, debounce::Debounce, throttle::Throttle, Binding};
use core::time::Duration;
let mut 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_from("typing...");Debounce vs Throttle:
- Debounce: Waits for a quiet period, useful for search input, API calls
- Throttle: Limits maximum update rate, useful for scroll events, animations
§Type-Erased 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);§Async Interop
Bridge async with reactive using adapters:
FutureSignal<T>:Option<T>becomesSome(T)when a future resolvesSignalStream<S>: treat aSignalas aStreamthat yields on updatesBindingMailbox<T>: cross-thread reactive state withget(),set(), andget_as()for type conversion
use 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::new(s);
// 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;§Debugging
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.
§Derive Macros
Enable the derive feature (enabled by default) to access:
#[derive(nami::Project)]: project a struct binding into bindings for each field
use 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 mut projected: PersonProjected = p.project();
projected.name.set_from("B"); // Automatic &str -> String conversion
assert_eq!(p.get().name, "B");Feature flags:
derive(default): re-exports macros fromnami-derivenative-executor(default): integrates withnative-executorfor mailbox helpers
§Notes
no_std: the crate is#![no_std]and usesalloc.- Keep watcher guards alive to remain subscribed; dropping the guard unsubscribes.
- Many examples are
no_runbecause they require an executor or side effects.
Modules§
- binding
- Reactive Bindings
- cache
- Cached Signal Implementation
- collection
- Reactive collections with watcher support.
- constant
- Constant Values for Reactive Computation
- debounce
- Debounce utilities for throttling signal updates.
- debug
- Debug utilities for Signal tracing
- distinct
- Distinct Signal Implementation
- future
- Future interop for reactive signals.
- map
- Map Module
- project
- Projection utilities for decomposing bindings into component parts.
- signal
- This module provides a framework for reactive computations that can track dependencies and automatically update when their inputs change.
- stream
- Stream interop for reactive signals.
- throttle
- Throttling utilities for limiting signal update rates.
- utils
- Addition Operations for Signal Types
- watcher
- Watcher Management
- zip
- Provides functionality for combining and transforming computations.
Macros§
- impl_
constant - Macro to implement the Signal trait for constant types.
- s
- Function-like procedural macro for creating formatted string signals with automatic variable capture.
Structs§
- Binding
- A
Binding<T>represents a mutable value of typeTthat can be observed. - Computed
- A wrapper around a boxed implementation of the
ComputedImpltrait. - Container
- A container for a value that can be observed.
Traits§
- Custom
Binding - The
CustomBindingtrait represents a computable value that can also be set. - Project
- Trait for projecting bindings into their component parts.
- Signal
- The core trait for reactive system.
- Signal
Ext - Extension trait providing convenient methods for all Signal types.
Functions§
- binding
- Creates a new binding from a value with automatic type conversion.
- constant
- Creates a new constant reactive value.
Derive Macros§
- Project
- Derive macro for implementing the
Projecttrait on structs.