| Crates.io | observe |
| lib.rs | observe |
| version | 2.0.0 |
| created_at | 2020-06-20 20:49:50.925322+00 |
| updated_at | 2026-01-20 07:02:42.652898+00 |
| description | Rust declarative observables inspired by MobX |
| homepage | https://github.com/s-panferov/observe |
| repository | https://github.com/s-panferov/observe |
| max_upload_size | |
| id | 256072 |
| size | 90,635 |
Declarative reactive programming for Rust, inspired by MobX.
cargo add observe
use observe::rc::{batch, Computed, Reaction, Var};
// Create reactive state
let count = Var::new(0);
// Create a derived value that automatically tracks dependencies
let doubled = Computed::new(Box::new({
let count = count.clone();
move |cx| count.get(cx) * 2
}));
// Create a reaction that runs when dependencies change
let reaction = Reaction::new(Box::new({
let doubled = doubled.clone();
move |cx| {
println!("Doubled value: {}", *doubled.get(cx));
}
}));
// Initial run
reaction.update(); // Prints: "Doubled value: 0"
// Update state inside a batch - reaction runs automatically
batch(|| {
count.set(5);
});
// Prints: "Doubled value: 10"
Var<T> holds mutable reactive state. When its value changes, all dependent computations and reactions are notified.
use observe::rc::Var;
// Create a new reactive variable
let name = Var::new(String::from("Alice"));
// Read value without tracking (useful outside reactive context)
assert_eq!(name.get_once(), "Alice");
// Update the value
name.set(String::from("Bob"));
// Update with a function
name.update(|s| s.push_str("!"));
// Replace and get old value
let old = name.replace(String::from("Charlie"));
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<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 observe::rc::{Computed, Var};
let first_name = Var::new(String::from("John"));
let last_name = Var::new(String::from("Doe"));
let full_name = Computed::new(Box::new({
let first_name = first_name.clone();
let last_name = last_name.clone();
move |cx| {
format!("{} {}", first_name.get(cx), last_name.get(cx))
}
}));
// Read the computed value
assert_eq!(*full_name.get_once(), "John Doe");
// When a dependency changes, the computed value updates
first_name.set(String::from("Jane"));
assert_eq!(*full_name.get_once(), "Jane Doe");
Computed values are lazy - they only recompute when accessed after a dependency changes.
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 observe::rc::{batch, Reaction, Var};
let temperature = Var::new(20);
let reaction = Reaction::new(Box::new({
let temperature = temperature.clone();
move |cx| {
let temp = temperature.get(cx);
if temp > 30 {
println!("Warning: High temperature!");
}
}
}));
// Run the reaction initially
reaction.update();
// Reactions are triggered inside batch()
batch(|| {
temperature.set(35);
});
// Prints: "Warning: High temperature!"
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 = Computed::new(Box::new(|cx| {
let a = var_a.get(cx); // var_a is now a dependency
let b = var_b.get(cx); // var_b is now a dependency
a + b
}));
// Using get_once() does NOT track dependencies
let computed = Computed::new(Box::new(|cx| {
let a = var_a.get_once(); // NOT tracked as dependency
a + 10
}));
The batch() function groups multiple state changes together. Reactions only run once after the batch completes, even if multiple dependencies changed.
use observe::rc::{batch, Reaction, Var};
let a = Var::new(1);
let b = Var::new(2);
let reaction = Reaction::new(Box::new({
let a = a.clone();
let b = b.clone();
move |cx| {
println!("Sum: {}", a.get(cx) + b.get(cx));
}
}));
reaction.update(); // Prints: "Sum: 3"
// Without batching, this would trigger the reaction twice
// With batching, it only runs once at the end
batch(|| {
a.set(10);
b.set(20);
});
// 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.
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 observe::rc::{batch, Var};
let value = Var::new(42);
batch(|| {
value.set(42); // Same value - no reactions triggered
value.set(42); // Still the same - still no reactions
value.set(43); // Different! Reactions will run
});
This means your types must implement Hash:
use std::hash::Hash;
#[derive(Hash)]
struct User {
id: u64,
name: String,
}
let user = Var::new(User { id: 1, name: String::from("Alice") });
| 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 |
| 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 |
| 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 |
| Function | Description |
|---|---|
batch(fn) |
Execute a function, run affected reactions once at the end |
in_batch() |
Check if currently inside a batch |
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.
MIT