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}