observe
Declarative reactive programming for Rust, inspired by MobX.
Features
- Automatic dependency tracking - Dependencies are tracked at runtime, no manual subscriptions
- Fine-grained reactivity - Only affected computations re-run when state changes
- Hash-based change detection - Efficient change detection using value hashes
- Batching - Group multiple state changes, reactions run once at the end
- Zero boilerplate - Simple, intuitive API
Installation
Quick Start
use ;
// Create reactive state
let count = new;
// Create a derived value that automatically tracks dependencies
let doubled = new;
// Create a reaction that runs when dependencies change
let reaction = new;
// Initial run
reaction.update; // Prints: "Doubled value: 0"
// Update state inside a batch - reaction runs automatically
batch;
// Prints: "Doubled value: 10"
Core Concepts
Var - Reactive State
Var<T> holds mutable reactive state. When its value changes, all dependent computations and reactions are notified.
use Var;
// Create a new reactive variable
let name = new;
// Read value without tracking (useful outside reactive context)
assert_eq!;
// Update the value
name.set;
// Update with a function
name.update;
// Replace and get old value
let old = name.replace;
Important: Values must implement Hash. The hash is used to detect whether the value actually changed - if you set the same value, dependents won't be notified.
Computed - Derived Values
Computed<T> represents a value derived from other reactive values. It automatically tracks which Var or Computed values were accessed and recomputes only when those dependencies change.
use ;
let first_name = new;
let last_name = new;
let full_name = new;
// Read the computed value
assert_eq!;
// When a dependency changes, the computed value updates
first_name.set;
assert_eq!;
Computed values are lazy - they only recompute when accessed after a dependency changes.
Reaction - Side Effects
Reaction executes side effects when its dependencies change. Unlike Computed, reactions don't return a value - they perform actions like updating the DOM, logging, or making network requests.
use ;
let temperature = new;
let reaction = new;
// Run the reaction initially
reaction.update;
// Reactions are triggered inside batch()
batch;
// Prints: "Warning: High temperature!"
The Evaluation Context
The cx parameter (of type &Evaluation) passed to closures is the key to automatic dependency tracking. When you call .get(cx) on a Var or Computed, it registers that value as a dependency.
// Dependency tracking happens through cx
let computed = new;
// Using get_once() does NOT track dependencies
let computed = new;
Batching
The batch() function groups multiple state changes together. Reactions only run once after the batch completes, even if multiple dependencies changed.
use ;
let a = new;
let b = new;
let reaction = new;
reaction.update; // Prints: "Sum: 3"
// Without batching, this would trigger the reaction twice
// With batching, it only runs once at the end
batch;
// Prints: "Sum: 30" (only once!)
Note: Reactions must be triggered inside a batch(). Calling reaction.update() outside a batch is allowed for initial setup, but subsequent automatic updates require batching.
Change Detection
observe uses hash-based change detection. When you call set(), the new value's hash is compared to the old hash. If they match, no notifications are sent.
use ;
let value = new;
batch;
This means your types must implement Hash:
use Hash;
let user = new;
API Reference
Var
| Method | Description |
|---|---|
Var::new(value) |
Create a new reactive variable |
var.get(cx) |
Read value with dependency tracking (clones the value) |
var.get_ref(cx) |
Read value with dependency tracking (returns Ref<T>) |
var.get_once() |
Read value without tracking (clones the value) |
var.get_ref_once() |
Read value without tracking (returns Ref<T>) |
var.set(value) |
Set a new value |
var.replace(value) |
Set a new value, return the old one |
var.update(fn) |
Mutate the value with a function |
var.toggle() |
Toggle boolean values |
var.map(fn) |
Create a Computed that maps this value |
Computed
| Method | Description |
|---|---|
Computed::new(fn) |
Create a new computed value |
computed.get(cx) |
Read value with dependency tracking |
computed.get_once() |
Read value without tracking |
Reaction
| Method | Description |
|---|---|
Reaction::new(fn) |
Create a new reaction |
Reaction::new_with_name(name, fn) |
Create a named reaction (useful for debugging) |
reaction.update() |
Run the reaction if invalid |
reaction.update_unchecked() |
Run the reaction unconditionally |
Functions
| Function | Description |
|---|---|
batch(fn) |
Execute a function, run affected reactions once at the end |
in_batch() |
Check if currently inside a batch |
Thread Safety
The observe::rc module uses Rc and RefCell, making it suitable for single-threaded applications and WASM.
For multi-threaded applications, use observe::arc which provides the same API but uses Arc and parking_lot locks for thread safety. The arc module also includes Async<T> for async computations with tokio.
License
MIT