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 ;
// Create mutable reactive state with automatic type conversion
let mut counter: = binding;
let mut message: = binding; // &str -> String conversion
// Derive a new computation from it
let doubled = map;
// Read current values
assert_eq!;
assert_eq!;
// Update the source and observe derived changes
counter.set;
assert_eq!;
// set_from() also accepts Into<T> for ergonomic updates
message.set_from; // &str works directly!
The Signal Trait
All reactive values implement a single trait:
use ;
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 ;
// Automatic type conversion with Into trait
let mut text: = binding; // &str -> String
let mut counter: = binding; // Direct initialization
let mut bignum: = binding;
let items: = binding; // Vec<i32> binding
// set_from() also uses Into<T> for ergonomic updates
text.set_from; // Direct &str, no .into() needed
counter.set;
counter.add_assign;
assert_eq!;
// Works with type conversions
let mut bignum: = binding;
bignum.set;
Watchers
React to changes via watch. Keep the returned guard alive to stay subscribed.
use ;
let mut name: = binding;
let _guard = name.watch;
name.set_from;
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 ;
use ;
let a: = binding;
let b: = binding;
let sum = add;
assert_eq!;
let pair = zip;
assert_eq!;
let squared = map;
assert_eq!;
Rate Limiting: Debounce and Throttle
Control the rate of updates with debounce and throttle utilities:
use ;
use Duration;
let mut input: = binding;
// Debounce: delay updates until 300ms of quiet time
let debounced = new;
// Throttle: limit to at most one update per 100ms
let throttled = new;
// Both preserve reactivity while controlling update frequency
input.set_from;
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 ;
let c = 10_i32.computed;
assert_eq!;
let plus_one = c.map;
assert_eq!;
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 FutureSignal;
use 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 ;
// let s = /* some Signal */;
// let mut stream = SignalStream::new(s);
// while let Some(value) = stream.next().await { /* ... */ }
Enhanced Mailboxes (requires native-executor feature):
use ;
use Str; // Example non-Send type
// Create binding with non-Send type
let text_binding: = binding;
let mailbox = text_binding.mailbox;
// Convert to Send type for cross-thread usage
let owned_string: String = mailbox.get_as.await;
assert_eq!;
// Regular mailbox operations
mailbox.set.await;
Debugging
Enable structured logging to trace signal behavior during development:
use ;
let value: = binding;
// Log only value changes (most common)
let debug = changes;
// Log all operations (verbose mode)
let debug = verbose;
// Log specific operations
let debug = compute_only; // Only computations
let debug = watchers; // Watcher lifecycle
let debug = compute_and_changes; // Both computations and changes
// Use custom configuration
let debug = with_config;
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 ;
let p: = binding;
// The derive generates `PersonProjected`
let mut projected: PersonProjected = p.project;
projected.name.set_from; // Automatic &str -> String conversion
assert_eq!;
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.