1 unstable release

0.1.0-preview Apr 28, 2024

#579 in Data structures

Download history 170/week @ 2024-04-26 28/week @ 2024-05-03

198 downloads per month
Used in tachys

MIT license

190KB
5K SLoC

An implementation of a fine-grained reactive system.

Fine-grained reactivity is an approach to modeling the flow of data through an interactive application by composing together three categories of reactive primitives:

  1. Signals: atomic units of state, which can be directly mutated.
  2. Computations: derived values, which cannot be mutated directly but update whenever the signals they depend on change. These include both synchronous and asynchronous derived values.
  3. Effects: side effects that synchronize the reactive system with the non-reactive world outside it.

Signals and computations are "source" nodes in the reactive graph, because an observer can subscribe to them to respond to changes in their values. Effects and computations are "subscriber" nodes, because they can listen to changes in other values.

use reactive_graph::{
    computed::ArcMemo,
    effect::Effect,
    prelude::{Read, Set},
    signal::ArcRwSignal,
};

let count = ArcRwSignal::new(1);
let double_count = ArcMemo::new({
    let count = count.clone();
    move |_| *count.read() * 2
});

// the effect will run once initially
Effect::new(move |_| {
    println!("double_count = {}", *double_count.read());
});

// updating `count` will propagate changes to the dependencies,
// causing the effect to run again
count.set(2);

This reactivity is called "fine grained" because updating the value of a signal only affects the effects and computations that depend on its value, without requiring any diffing or update calculations for other values.

This model is especially suitable for building user interfaces, i.e., long-lived systems in which changes can begin from many different entry points. It is not particularly useful in "run-once" programs like a CLI.

Design Principles and Assumptions

  • Effects are expensive. The library is built on the assumption that the side effects (making a network request, rendering something to the DOM, writing to disk) are orders of magnitude more expensive than propagating signal updates. As a result, the algorithm is designed to avoid re-running side effects unnecessarily, and is willing to sacrifice a small amount of raw update speed to that goal.
  • Automatic dependency tracking. Dependencies are not specified as a compile-time list, but tracked at runtime. This in turn enables dynamic dependency tracking: subscribers unsubscribe from their sources between runs, which means that a subscriber that contains a condition branch will not re-run when dependencies update that are only used in the inactive branch.
  • Asynchronous effect scheduling. Effects are spawned as asynchronous tasks. This means that while updating a signal will immediately update its value, effects that depend on it will not run until the next "tick" of the async runtime. (This in turn means that the reactive system is async runtime agnostic: it can be used in the browser with wasm-bindgen-futures, in a native binary with tokio, in a GTK application with glib, etc.)

The reactive-graph algorithm used in this crate is based on that of Reactively, as described in this article.

Dependencies

~1.3–2MB
~41K SLoC