#reactive #ui #react #cell #vue

reax

A reactivity system for Rust that infers dependencies between functions

2 unstable releases

0.2.0 Jul 5, 2020
0.1.0 Jan 23, 2020

#846 in Rust patterns

MIT license

82KB
1.5K SLoC

Reax

Reax is a reactivity system for Rust that infers dependencies between functions.

Every Variable managed by reax is a node in a dependency graph. Changes to and usages of variables are tracked by the global ThreadRuntime which updates the graph to reflect actual variable accesses. There are two kinds of built-in variables:

  • Var which can be explicitly mutated.
  • ComputedVar which lazily computes its value with a function.

Users can listen to and react to changes of variables using .watch.

Critically, a ComputedVar will only re-compute when needed. If, when computing its value, a ComputedVar uses any other variable anywhere (directly or indirectly), changes to any of those upstream variables will automatically mark the computed variable as dirty and the variable's value will be recomputed the next time it is used.

Examples

Reax builds a model of how the variables in your program interact as it runs.

use reax::prelude::*;

// Create input variables.
let number = Var::new(1).with_label("number");
let name = Var::new("Sam").with_label("name");

// Create computed variables.
let formatted = (&number)
   .map(|x| format!("{}", x))
   .with_label("formatted");

let printout = computed! {
    output! text = String::new(); // Reuse a buffer

    text.clear();
    *text += *name.get();
    *text += " sees ";
    *text += formatted.get().as_str();
}.with_label("printout");

// The computed variables haven't been used yet. Nothing is hooked-up.
assert_eq!(printout.node().depends_on(formatted.node()), false);

// Use the variables!
assert_eq!(printout.get().as_str(), "Sam sees 1");
number.set(42);
name.set("Reax");
assert_eq!(printout.get().as_str(), "Reax sees 42");

// Reax now knows how data moves through the variables!
assert_eq!(printout.node().depends_on(formatted.node()), true);

// Print a .dot visualization.
reax::ThreadRuntime::write_graphviz(std::io::stdout().lock()).unwrap();

We can see this example through reax's eyes:

Dependency Graph


Reax will only update computed variables when needed.

use reax::prelude::*;

let number = Var::new(0);
let bigger_number = (&number).map(|x| *x + 10);
let even_bigger_number = (&bigger_number).map(|x| *x + 100);
let times_called = Var::new(0);

// Set up a watcher to track how often bigger_number changes.
let mut eval = EagerCompute::new(());
eval.watch(&bigger_number, |_| {
   *times_called.mutate() += 1;
});

// The watcher is called once on creation.
assert_eq!(*times_called.get(), 1);

// Run the watcher. This is effectively a no-op since nothing has changed.
for _ in 0..100 { eval.tick(); }

// Update a variable.
number.set(1);

// Dependent variables are instantly dirty.
assert_eq!(bigger_number.node().is_dirty(), true);
assert_eq!(even_bigger_number.node().is_dirty(), true);

// Run the watcher again. This time it fires.
eval.tick();
assert_eq!(*times_called.get(), 2);

// even_bigger_number is still dirty since no one has used it yet.
assert_eq!(even_bigger_number.node().is_dirty(), true);

Here you can see how the variables downstream from number are all instantly marked when it changes. But they won't be recomputed until used:

Dependency Graph


Reax has no built-in understanding of collections so you can use nested Vars to better control the "depth" of changes.

use reax::prelude::*;

// Create a list of variables.
let list = Var::new(Vec::new());
for x in 1..=3 {
    list.mutate().push(Var::new(x));
}

// Some computed properties:
let length = computed! { list.get().len() };
let sum = computed! {
    list.get().iter().map(|elem| *elem.get()).sum::<i32>()
};

// Make length and sum outdated by pushing an extra element.
list.mutate().push(Var::new(4));

// Update the length.
length.check(&mut ());

// Now only make sum outdated, and leave it that way.
list.get()[0].set(100);

Visualizing the runtime at the end of this example, you can see that only the sum is dirty. None of the list elements are dependencies of the list itself so any changes to them don't effect variables that never read them. And reax hasn't seen that the extra element will be used in the sum. It will find that out the next time the sum is computed.

Dependency Graph

Dependencies