1//! [`Root`] and [`Scope`].
23use std::cell::{Cell, RefCell};
45use slotmap::{Key, SlotMap};
6use smallvec::SmallVec;
78use crate::*;
910/// The struct managing the state of the reactive system. Only one should be created per running
11/// app.
12///
13/// Often times, this is intended to be leaked to be able to get a `&'static Root`. However, the
14/// `Root` is also `dispose`-able, meaning that any resources allocated in this `Root` will get
15/// deallocated. Therefore in practice, there should be no memory leak at all except for the `Root`
16/// itself. Finally, the `Root` is expected to live for the whole duration of the app so this is
17/// not a problem.
18pub(crate) struct Root {
19/// If this is `Some`, that means we are tracking signal accesses.
20pub tracker: RefCell<Option<DependencyTracker>>,
21/// A temporary buffer used in `propagate_updates` to prevent allocating a new Vec every time
22 /// it is called.
23pub rev_sorted_buf: RefCell<Vec<NodeId>>,
24/// The current node that owns everything created in its scope.
25 /// If we are at the top-level, then this is the "null" key.
26pub current_node: Cell<NodeId>,
27/// The root node of the reactive graph.
28pub root_node: Cell<NodeId>,
29/// All the nodes created in this `Root`.
30pub nodes: RefCell<SlotMap<NodeId, ReactiveNode>>,
31/// A list of signals who need their values to be propagated after the batch is over.
32pub node_update_queue: RefCell<Vec<NodeId>>,
33/// Whether we are currently batching signal updatse. If this is true, we do not run
34 /// `effect_queue` and instead wait until the end of the batch.
35pub batching: Cell<bool>,
36}
3738thread_local! {
39/// The current reactive root.
40static GLOBAL_ROOT: Cell<Option<&'static Root>> = const { Cell::new(None) };
41}
4243impl Root {
44/// Get the current reactive root. Panics if no root is found.
45#[cfg_attr(debug_assertions, track_caller)]
46pub fn global() -> &'static Root {
47 GLOBAL_ROOT.with(|root| root.get()).expect("no root found")
48 }
4950/// Sets the current reactive root. Returns the previous root.
51pub fn set_global(root: Option<&'static Root>) -> Option<&'static Root> {
52 GLOBAL_ROOT.with(|r| r.replace(root))
53 }
5455/// Create a new reactive root. This root is leaked and so lives until the end of the program.
56pub fn new_static() -> &'static Self {
57let this = Self {
58 tracker: RefCell::new(None),
59 rev_sorted_buf: RefCell::new(Vec::new()),
60 current_node: Cell::new(NodeId::null()),
61 root_node: Cell::new(NodeId::null()),
62 nodes: RefCell::new(SlotMap::default()),
63 node_update_queue: RefCell::new(Vec::new()),
64 batching: Cell::new(false),
65 };
66let _ref = Box::leak(Box::new(this));
67 _ref.reinit();
68 _ref
69 }
7071/// Disposes of all the resources held on by this root and resets the state.
72pub fn reinit(&'static self) {
73// Dispose the root node.
74NodeHandle(self.root_node.get(), self).dispose();
7576let _ = self.tracker.take();
77let _ = self.rev_sorted_buf.take();
78let _ = self.node_update_queue.take();
79let _ = self.current_node.take();
80let _ = self.root_node.take();
81let _ = self.nodes.take();
82self.batching.set(false);
8384// Create a new root node.
85Root::set_global(Some(self));
86let root_node = create_child_scope(|| {});
87 Root::set_global(None);
88self.root_node.set(root_node.0);
89self.current_node.set(root_node.0);
90 }
9192/// Create a new child scope. Implementation detail for [`create_child_scope`].
93pub fn create_child_scope(&'static self, f: impl FnOnce()) -> NodeHandle {
94let node = create_signal(()).id;
95let prev = self.current_node.replace(node);
96 f();
97self.current_node.set(prev);
98 NodeHandle(node, self)
99 }
100101/// Run the provided closure in a tracked scope. This will detect all the signals that are
102 /// accessed and track them in a dependency list.
103pub fn tracked_scope<T>(&self, f: impl FnOnce() -> T) -> (T, DependencyTracker) {
104let prev = self.tracker.replace(Some(DependencyTracker::default()));
105let ret = f();
106 (ret, self.tracker.replace(prev).unwrap())
107 }
108109/// Run the update callback of the signal, also recreating any dependencies found by
110 /// tracking signal accesses inside the function.
111 ///
112 /// Also marks all the dependencies as dirty and marks the current node as clean.
113 ///
114 /// # Params
115 /// * `root` - The reactive root.
116 /// * `id` - The id associated with the reactive node. `SignalId` inside the state itself.
117fn run_node_update(&'static self, current: NodeId) {
118debug_assert_eq!(
119self.nodes.borrow()[current].state,
120 NodeState::Dirty,
121"should only update when dirty"
122);
123// Remove old dependency links.
124let dependencies = std::mem::take(&mut self.nodes.borrow_mut()[current].dependencies);
125for dependency in dependencies {
126self.nodes.borrow_mut()[dependency]
127 .dependents
128 .retain(|&id| id != current);
129 }
130// We take the callback out because that requires a mut ref and we cannot hold that while
131 // running update itself.
132let mut nodes_mut = self.nodes.borrow_mut();
133let mut callback = nodes_mut[current].callback.take().unwrap();
134let mut value = nodes_mut[current].value.take().unwrap();
135 drop(nodes_mut); // End RefMut borrow.
136137NodeHandle(current, self).dispose_children(); // Destroy anything created in a previous update.
138139let prev = self.current_node.replace(current);
140let (changed, tracker) = self.tracked_scope(|| callback(&mut value));
141self.current_node.set(prev);
142143 tracker.create_dependency_link(self, current);
144145let mut nodes_mut = self.nodes.borrow_mut();
146 nodes_mut[current].callback = Some(callback); // Put the callback back in.
147nodes_mut[current].value = Some(value);
148149// Mark this node as clean.
150nodes_mut[current].state = NodeState::Clean;
151 drop(nodes_mut);
152153if changed {
154self.mark_dependents_dirty(current);
155 }
156 }
157158// Mark any dependent node of the current node as dirty.
159fn mark_dependents_dirty(&self, current: NodeId) {
160let mut nodes_mut = self.nodes.borrow_mut();
161let dependents = std::mem::take(&mut nodes_mut[current].dependents);
162for &dependent in &dependents {
163if let Some(dependent) = nodes_mut.get_mut(dependent) {
164 dependent.state = NodeState::Dirty;
165 }
166 }
167 nodes_mut[current].dependents = dependents;
168 }
169170/// If there are no cyclic dependencies, then the reactive graph is a DAG (Directed Acylic
171 /// Graph). We can therefore use DFS to get a topological sorting of all the reactive nodes.
172 ///
173 /// We then go through every node in this topological sorting and update only those nodes which
174 /// have dependencies that were updated.
175fn propagate_node_updates(&'static self, start_nodes: &[NodeId]) {
176// Try to reuse the shared buffer if possible.
177let mut rev_sorted = Vec::new();
178let mut rev_sorted_buf = self.rev_sorted_buf.try_borrow_mut();
179let rev_sorted = if let Ok(rev_sorted_buf) = rev_sorted_buf.as_mut() {
180 rev_sorted_buf.clear();
181 rev_sorted_buf
182 } else {
183&mut rev_sorted
184 };
185186// Traverse reactive graph.
187for &node in start_nodes {
188Self::dfs(node, &mut self.nodes.borrow_mut(), rev_sorted);
189self.mark_dependents_dirty(node);
190 }
191192for &node in rev_sorted.iter().rev() {
193let mut nodes_mut = self.nodes.borrow_mut();
194// Only run if node is still alive.
195if nodes_mut.get(node).is_none() {
196continue;
197 }
198let node_state = &mut nodes_mut[node];
199 node_state.mark = Mark::None; // Reset value.
200201 // Check if this node needs to be updated.
202if nodes_mut[node].state == NodeState::Dirty {
203 drop(nodes_mut); // End RefMut borrow.
204self.run_node_update(node)
205 };
206 }
207 }
208209/// Call this if `start_node` has been updated manually. This will automatically update all
210 /// signals that depend on `start_node`.
211 ///
212 /// If we are currently batching, defers updating the signal until the end of the batch.
213pub fn propagate_updates(&'static self, start_node: NodeId) {
214if self.batching.get() {
215self.node_update_queue.borrow_mut().push(start_node);
216 } else {
217// Set the global root.
218let prev = Root::set_global(Some(self));
219// Propagate any signal updates.
220self.propagate_node_updates(&[start_node]);
221 Root::set_global(prev);
222 }
223 }
224225/// Run depth-first-search on the reactive graph starting at `current`.
226fn dfs(current_id: NodeId, nodes: &mut SlotMap<NodeId, ReactiveNode>, buf: &mut Vec<NodeId>) {
227let Some(current) = nodes.get_mut(current_id) else {
228// If signal is dead, don't even visit it.
229return;
230 };
231232match current.mark {
233 Mark::Temp => panic!("cyclic reactive dependency"),
234 Mark::Permanent => return,
235 Mark::None => {}
236 }
237 current.mark = Mark::Temp;
238239// Take the `dependents` field out temporarily to avoid borrow checker.
240let children = std::mem::take(&mut current.dependents);
241for child in &children {
242Self::dfs(*child, nodes, buf);
243 }
244 nodes[current_id].dependents = children;
245246 nodes[current_id].mark = Mark::Permanent;
247 buf.push(current_id);
248 }
249250/// Sets the batch flag to `true`.
251fn start_batch(&self) {
252self.batching.set(true);
253 }
254255/// Sets the batch flag to `false` and run all the queued effects.
256fn end_batch(&'static self) {
257self.batching.set(false);
258let nodes = self.node_update_queue.take();
259self.propagate_node_updates(&nodes);
260 }
261}
262263/// A handle to a root. This lets you reinitialize or dispose the root for resource cleanup.
264///
265/// This is generally obtained from [`create_root`].
266#[derive(Clone, Copy)]
267pub struct RootHandle {
268 _ref: &'static Root,
269}
270271impl RootHandle {
272/// Destroy everything that was created in this scope.
273pub fn dispose(&self) {
274self._ref.reinit();
275 }
276277/// Runs the closure in the current scope of the root.
278pub fn run_in<T>(&self, f: impl FnOnce() -> T) -> T {
279let prev = Root::set_global(Some(self._ref));
280let ret = f();
281 Root::set_global(prev);
282 ret
283 }
284}
285286/// Tracks nodes that are accessed inside a reactive scope.
287#[derive(Default)]
288pub(crate) struct DependencyTracker {
289/// A list of reactive nodes that were accessed.
290pub dependencies: SmallVec<[NodeId; 1]>,
291}
292293impl DependencyTracker {
294/// Sets the `dependents` field for all the nodes that have been tracked and updates
295 /// `dependencies` of the `dependent`.
296pub fn create_dependency_link(self, root: &Root, dependent: NodeId) {
297for node in &self.dependencies {
298 root.nodes.borrow_mut()[*node].dependents.push(dependent);
299 }
300// Set the signal dependencies so that it is updated automatically.
301root.nodes.borrow_mut()[dependent].dependencies = self.dependencies;
302 }
303}
304305/// Creates a new reactive root with a top-level reactive node. The returned [`RootHandle`] can be
306/// used to [`dispose`](RootHandle::dispose) the root.
307///
308/// # Example
309/// ```rust
310/// # use sycamore_reactive::*;
311///
312/// create_root(|| {
313/// let signal = create_signal(123);
314///
315/// let child_scope = create_child_scope(move || {
316/// // ...
317/// });
318/// });
319/// ```
320#[must_use = "root should be disposed"]
321pub fn create_root(f: impl FnOnce()) -> RootHandle {
322let _ref = Root::new_static();
323#[cfg(not(target_arch = "wasm32"))]
324{
325/// An unsafe wrapper around a raw pointer which we promise to never touch, effectively
326 /// making it thread-safe.
327#[allow(dead_code)]
328struct UnsafeSendPtr<T>(*const T);
329/// We never ever touch the pointer inside so surely this is safe!
330unsafe impl<T> Send for UnsafeSendPtr<T> {}
331332/// A static variable to keep on holding to the allocated `Root`s to prevent Miri and
333 /// Valgrind from complaining.
334static KEEP_ALIVE: std::sync::Mutex<Vec<UnsafeSendPtr<Root>>> =
335 std::sync::Mutex::new(Vec::new());
336 KEEP_ALIVE
337 .lock()
338 .unwrap()
339 .push(UnsafeSendPtr(_ref as *const Root));
340 }
341342 Root::set_global(Some(_ref));
343 NodeHandle(_ref.root_node.get(), _ref).run_in(f);
344 Root::set_global(None);
345 RootHandle { _ref }
346}
347348/// Create a child scope.
349///
350/// Returns the created [`NodeHandle`] which can be used to dispose it.
351#[cfg_attr(debug_assertions, track_caller)]
352pub fn create_child_scope(f: impl FnOnce()) -> NodeHandle {
353 Root::global().create_child_scope(f)
354}
355356/// Adds a callback that is called when the scope is destroyed.
357///
358/// # Example
359/// ```rust
360/// # use sycamore_reactive::*;
361/// # create_root(|| {
362/// let child_scope = create_child_scope(|| {
363/// on_cleanup(|| {
364/// println!("Child scope is being dropped");
365/// });
366/// });
367/// child_scope.dispose(); // Executes the on_cleanup callback.
368/// # });
369/// ```
370pub fn on_cleanup(f: impl FnOnce() + 'static) {
371let root = Root::global();
372if !root.current_node.get().is_null() {
373 root.nodes.borrow_mut()[root.current_node.get()]
374 .cleanups
375 .push(Box::new(f));
376 }
377}
378379/// Batch updates from related signals together and only run memos and effects at the end of the
380/// scope.
381///
382/// # Example
383///
384/// ```
385/// # use sycamore_reactive::*;
386/// # let _ = create_root(|| {
387/// let state = create_signal(1);
388/// let double = create_memo(move || state.get() * 2);
389/// batch(move || {
390/// state.set(2);
391/// assert_eq!(double.get(), 2);
392/// });
393/// assert_eq!(double.get(), 4);
394/// # });
395/// ```
396pub fn batch<T>(f: impl FnOnce() -> T) -> T {
397let root = Root::global();
398 root.start_batch();
399let ret = f();
400 root.end_batch();
401 ret
402}
403404/// Run the passed closure inside an untracked dependency scope.
405///
406/// See also [`ReadSignal::get_untracked`].
407///
408/// # Example
409///
410/// ```
411/// # use sycamore_reactive::*;
412/// # create_root(|| {
413/// let state = create_signal(1);
414/// let double = create_memo(move || untrack(|| state.get() * 2));
415/// assert_eq!(double.get(), 2);
416///
417/// state.set(2);
418/// // double value should still be old value because state was untracked
419/// assert_eq!(double.get(), 2);
420/// # });
421/// ```
422pub fn untrack<T>(f: impl FnOnce() -> T) -> T {
423 untrack_in_scope(f, Root::global())
424}
425426/// Same as [`untrack`] but for a specific [`Root`].
427pub(crate) fn untrack_in_scope<T>(f: impl FnOnce() -> T, root: &'static Root) -> T {
428let prev = root.tracker.replace(None);
429let ret = f();
430 root.tracker.replace(prev);
431 ret
432}
433434/// Get a handle to the current reactive scope.
435pub fn use_current_scope() -> NodeHandle {
436let root = Root::global();
437 NodeHandle(root.current_node.get(), root)
438}
439440/// Get a handle to the root reactive scope.
441pub fn use_global_scope() -> NodeHandle {
442let root = Root::global();
443 NodeHandle(root.root_node.get(), root)
444}
445446#[cfg(test)]
447mod tests {
448use crate::*;
449450#[test]
451fn cleanup() {
452let _ = create_root(|| {
453let cleanup_called = create_signal(false);
454let scope = create_child_scope(|| {
455 on_cleanup(move || {
456 cleanup_called.set(true);
457 });
458 });
459assert!(!cleanup_called.get());
460 scope.dispose();
461assert!(cleanup_called.get());
462 });
463 }
464465#[test]
466fn cleanup_in_effect() {
467let _ = create_root(|| {
468let trigger = create_signal(());
469470let counter = create_signal(0);
471472 create_effect(move || {
473 trigger.track();
474475 on_cleanup(move || {
476 counter.set(counter.get() + 1);
477 });
478 });
479480assert_eq!(counter.get(), 0);
481482 trigger.set(());
483assert_eq!(counter.get(), 1);
484485 trigger.set(());
486assert_eq!(counter.get(), 2);
487 });
488 }
489490#[test]
491fn cleanup_is_untracked() {
492let _ = create_root(|| {
493let trigger = create_signal(());
494495let counter = create_signal(0);
496497 create_effect(move || {
498 counter.set(counter.get_untracked() + 1);
499500 on_cleanup(move || {
501 trigger.track(); // trigger should not be tracked
502});
503 });
504505assert_eq!(counter.get(), 1);
506507 trigger.set(());
508assert_eq!(counter.get(), 1);
509 });
510 }
511512#[test]
513fn batch_memo() {
514let _ = create_root(|| {
515let state = create_signal(1);
516let double = create_memo(move || state.get() * 2);
517 batch(move || {
518 state.set(2);
519assert_eq!(double.get(), 2);
520 });
521assert_eq!(double.get(), 4);
522 });
523 }
524525#[test]
526fn batch_updates_effects_at_end() {
527let _ = create_root(|| {
528let state1 = create_signal(1);
529let state2 = create_signal(2);
530let counter = create_signal(0);
531 create_effect(move || {
532 counter.set(counter.get_untracked() + 1);
533let _ = state1.get() + state2.get();
534 });
535assert_eq!(counter.get(), 1);
536 state1.set(2);
537 state2.set(3);
538assert_eq!(counter.get(), 3);
539 batch(move || {
540 state1.set(3);
541assert_eq!(counter.get(), 3);
542 state2.set(4);
543assert_eq!(counter.get(), 3);
544 });
545assert_eq!(counter.get(), 4);
546 });
547 }
548}