Expand description
§Tincan - A Fast, Lock-Free Reactive Primitives Library
Tincan provides fine-grained reactivity with signals, computed values (memos), and side effects. It’s designed for building reactive applications with automatic dependency tracking and efficient updates.
§Overview
Tincan uses a context object pattern where all reactive primitives are created
through a Scope. This provides explicit ownership, automatic cleanup, and
lock-free performance for single-threaded contexts.
§Core Concepts
Scope: The reactive context that owns all reactive primitivesSignal: Mutable reactive state that notifies observers when changedMemo: Computed values that cache results and only recompute when dependencies changeEffect: Side effects that automatically re-run when dependencies change
§Quick Start
use tincan::Scope;
// Create a reactive scope
let cx = Scope::new();
// Create a signal (reactive state)
let count = cx.signal(0);
// Create a memo (computed value)
let doubled = cx.memo({
let count = count.clone();
move || count.get() * 2
});
// Create an effect (side effect)
let count_clone = count.clone();
let doubled_clone = doubled.clone();
cx.effect(move || {
println!("Count: {}, Doubled: {}", count_clone.get(), doubled_clone.get());
});
// Prints: Count: 0, Doubled: 0
// Update the signal
count.set(5);
// Effect runs again, prints: Count: 5, Doubled: 10§Features
§Automatic Dependency Tracking
Effects and memos automatically track which signals they read:
use tincan::Scope;
let cx = Scope::new();
let first = cx.signal("John");
let last = cx.signal("Doe");
let full_name = cx.memo({
let first = first.clone();
let last = last.clone();
move || format!("{} {}", first.get(), last.get())
});
assert_eq!(full_name.get(), "John Doe");
first.set("Jane");
assert_eq!(full_name.get(), "Jane Doe");§Efficient Updates
Memos cache their values and only recompute when dependencies change:
use tincan::Scope;
use std::cell::Cell;
use std::rc::Rc;
let cx = Scope::new();
let input = cx.signal(5);
let compute_count = Rc::new(Cell::new(0));
let expensive = cx.memo({
let input = input.clone();
let compute_count = Rc::clone(&compute_count);
move || {
compute_count.set(compute_count.get() + 1);
input.get() * 2
}
});
assert_eq!(expensive.get(), 10);
assert_eq!(compute_count.get(), 1);
// Reading again uses cached value
assert_eq!(expensive.get(), 10);
assert_eq!(compute_count.get(), 1); // Not recomputed!
// Only recomputes when input changes
input.set(10);
assert_eq!(expensive.get(), 20);
assert_eq!(compute_count.get(), 2); // Recomputed once§Isolation and Cleanup
Scopes provide automatic cleanup and isolation:
use tincan::Scope;
// Each scope is completely independent
let cx1 = Scope::new();
let signal1 = cx1.signal(1);
let cx2 = Scope::new();
let signal2 = cx2.signal(2);
// They don't interfere with each other
assert_eq!(signal1.get(), 1);
assert_eq!(signal2.get(), 2);
// Dropping the scope cleans up all resources
drop(cx1);
// signal1 is now unusable (would panic if accessed)§Signal Combinators
Transform and combine signals:
use tincan::Scope;
let cx = Scope::new();
let celsius = cx.signal(0);
// Map: Transform a signal
let fahrenheit = celsius.map(|c| c * 9 / 5 + 32);
assert_eq!(fahrenheit.get(), 32);
celsius.set(100);
assert_eq!(fahrenheit.get(), 212);
// Zip: Combine two signals
let width = cx.signal(10);
let height = cx.signal(5);
let area = width.clone().zip(height).map(|(w, h)| w * h);
assert_eq!(area.get(), 50);§Watching for Changes
React to specific signal changes:
use tincan::Scope;
use std::cell::Cell;
use std::rc::Rc;
let cx = Scope::new();
let count = cx.signal(0);
let call_count = Rc::new(Cell::new(0));
let call_count_clone = Rc::clone(&call_count);
let _guard = count.watch(move |_value| {
call_count_clone.set(call_count_clone.get() + 1);
});
assert_eq!(call_count.get(), 1); // Called immediately
count.set(5);
assert_eq!(call_count.get(), 2); // Called on change§Testing
Scopes make testing easy:
use tincan::Scope;
#[test]
fn test_counter() {
let cx = Scope::new();
let count = cx.signal(0);
count.set(42);
assert_eq!(count.get(), 42);
} // Scope dropped, everything cleaned up§Examples
See the examples/ directory for more complete examples:
basic_signals.rs- Simple signal operationseffects.rs- Working with effectsmemos.rs- Computed values and caching
Re-exports§
pub use effect::Effect;pub use memo::Memo;pub use runtime::Scope;pub use signal::Signal;pub use signal::WatchGuard;