Skip to main content

reactive_graph/
lib.rs

1//! An implementation of a fine-grained reactive system.
2//!
3//! Fine-grained reactivity is an approach to modeling the flow of data through an interactive
4//! application by composing together three categories of reactive primitives:
5//! 1. **Signals**: atomic units of state, which can be directly mutated.
6//! 2. **Computations**: derived values, which cannot be mutated directly but update whenever the signals
7//!    they depend on change. These include both synchronous and asynchronous derived values.
8//! 3. **Effects**: side effects that synchronize the reactive system with the non-reactive world
9//!    outside it.
10//!
11//! Signals and computations are "source" nodes in the reactive graph, because an observer can
12//! subscribe to them to respond to changes in their values. Effects and computations are "subscriber"
13//! nodes, because they can listen to changes in other values.
14//!
15//! ```rust
16//! # any_spawner::Executor::init_futures_executor();
17//! # let owner = reactive_graph::owner::Owner::new(); owner.set();
18//! use reactive_graph::{
19//!     computed::ArcMemo,
20//!     effect::Effect,
21//!     prelude::{Read, Set},
22//!     signal::ArcRwSignal,
23//! };
24//!
25//! let count = ArcRwSignal::new(1);
26//! let double_count = ArcMemo::new({
27//!     let count = count.clone();
28//!     move |_| *count.read() * 2
29//! });
30//!
31//! // the effect will run once initially
32//! Effect::new(move |_| {
33//!     println!("double_count = {}", *double_count.read());
34//! });
35//!
36//! // updating `count` will propagate changes to the dependencies,
37//! // causing the effect to run again
38//! count.set(2);
39//! ```
40//!
41//! This reactivity is called "fine grained" because updating the value of a signal only affects
42//! the effects and computations that depend on its value, without requiring any diffing or update
43//! calculations for other values.
44//!
45//! This model is especially suitable for building user interfaces, i.e., long-lived systems in
46//! which changes can begin from many different entry points. It is not particularly useful in
47//! "run-once" programs like a CLI.
48//!
49//! ## Design Principles and Assumptions
50//! - **Effects are expensive.** The library is built on the assumption that the side effects
51//!   (making a network request, rendering something to the DOM, writing to disk) are orders of
52//!   magnitude more expensive than propagating signal updates. As a result, the algorithm is
53//!   designed to avoid re-running side effects unnecessarily, and is willing to sacrifice a small
54//!   amount of raw update speed to that goal.
55//! - **Automatic dependency tracking.** Dependencies are not specified as a compile-time list, but
56//!   tracked at runtime. This in turn enables **dynamic dependency tracking**: subscribers
57//!   unsubscribe from their sources between runs, which means that a subscriber that contains a
58//!   condition branch will not re-run when dependencies update that are only used in the inactive
59//!   branch.
60//! - **Asynchronous effect scheduling.** Effects are spawned as asynchronous tasks. This means
61//!   that while updating a signal will immediately update its value, effects that depend on it
62//!   will not run until the next "tick" of the async runtime. (This in turn means that the
63//!   reactive system is *async runtime agnostic*: it can be used in the browser with
64//!   `wasm-bindgen-futures`, in a native binary with `tokio`, in a GTK application with `glib`,
65//!   etc.)
66//!
67//! The reactive-graph algorithm used in this crate is based on that of
68//! [Reactively](https://github.com/modderme123/reactively), as described
69//! [in this article](https://dev.to/modderme123/super-charging-fine-grained-reactive-performance-47ph).
70
71#![cfg_attr(all(feature = "nightly", rustc_nightly), feature(unboxed_closures))]
72#![cfg_attr(all(feature = "nightly", rustc_nightly), feature(fn_traits))]
73#![deny(missing_docs)]
74
75use std::{fmt::Arguments, future::Future};
76
77pub mod actions;
78pub(crate) mod channel;
79pub mod computed;
80pub mod diagnostics;
81pub mod effect;
82pub mod graph;
83pub mod owner;
84pub mod send_wrapper_ext;
85#[cfg(feature = "serde")]
86mod serde;
87pub mod signal;
88mod trait_options;
89pub mod traits;
90pub mod transition;
91pub mod wrappers;
92
93mod into_reactive_value;
94pub use into_reactive_value::*;
95
96/// A standard way to wrap functions and closures to pass them to components.
97pub mod callback;
98
99use computed::ScopedFuture;
100
101#[cfg(all(feature = "nightly", rustc_nightly))]
102mod nightly;
103
104/// Reexports frequently-used traits.
105pub mod prelude {
106    pub use crate::{
107        into_reactive_value::IntoReactiveValue, owner::FromLocal, traits::*,
108    };
109}
110
111// TODO remove this, it's just useful while developing
112#[allow(unused)]
113#[doc(hidden)]
114pub fn log_warning(text: Arguments) {
115    #[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
116    {
117        web_sys::console::warn_1(&text.to_string().into());
118    }
119    #[cfg(all(
120        not(feature = "tracing"),
121        not(all(target_arch = "wasm32", target_os = "unknown"))
122    ))]
123    {
124        eprintln!("{text}");
125    }
126}
127
128/// Calls [`Executor::spawn`](any_spawner::Executor::spawn) on non-wasm targets and [`Executor::spawn_local`](any_spawner::Executor::spawn_local) on wasm targets, but ensures that the task also runs in the current arena, if
129/// multithreaded arena sandboxing is enabled.
130pub fn spawn(task: impl Future<Output = ()> + Send + 'static) {
131    #[cfg(feature = "sandboxed-arenas")]
132    let task = owner::Sandboxed::new(task);
133
134    #[cfg(not(target_family = "wasm"))]
135    any_spawner::Executor::spawn(task);
136
137    #[cfg(target_family = "wasm")]
138    any_spawner::Executor::spawn_local(task);
139}
140
141/// Calls [`Executor::spawn_local`](any_spawner::Executor::spawn_local), but ensures that the task also runs in the current arena, if
142/// multithreaded arena sandboxing is enabled.
143pub fn spawn_local(task: impl Future<Output = ()> + 'static) {
144    #[cfg(feature = "sandboxed-arenas")]
145    let task = owner::Sandboxed::new(task);
146
147    any_spawner::Executor::spawn_local(task);
148}
149
150/// Calls [`Executor::spawn_local`](any_spawner::Executor), but ensures that the task runs under the current reactive [`Owner`](crate::owner::Owner) and observer.
151///
152/// Does not cancel the task if the owner is cleaned up.
153pub fn spawn_local_scoped(task: impl Future<Output = ()> + 'static) {
154    let task = ScopedFuture::new(task);
155
156    #[cfg(feature = "sandboxed-arenas")]
157    let task = owner::Sandboxed::new(task);
158
159    any_spawner::Executor::spawn_local(task);
160}
161
162/// Calls [`Executor::spawn_local`](any_spawner::Executor), but ensures that the task runs under the current reactive [`Owner`](crate::owner::Owner) and observer.
163///
164/// Cancels the task if the owner is cleaned up.
165pub fn spawn_local_scoped_with_cancellation(
166    task: impl Future<Output = ()> + 'static,
167) {
168    use crate::owner::on_cleanup;
169    use futures::future::{AbortHandle, Abortable};
170
171    let (abort_handle, abort_registration) = AbortHandle::new_pair();
172    on_cleanup(move || abort_handle.abort());
173
174    let task = Abortable::new(task, abort_registration);
175    let task = ScopedFuture::new(task);
176
177    #[cfg(feature = "sandboxed-arenas")]
178    let task = owner::Sandboxed::new(task);
179
180    any_spawner::Executor::spawn_local(async move {
181        _ = task.await;
182    });
183}