tincan 0.3.0

A lightweight reactive state management library for Rust.
Documentation
use crate::runtime::RuntimeInner;
use crate::{Effect, Memo, Signal};
use std::cell::RefCell;
use std::rc::Rc;

/// A reactive scope that owns a dependency graph.
///
/// All reactive primitives (signals, memos, effects) are created through a scope.
/// The scope automatically cleans up all resources when dropped, providing
/// isolation between different reactive contexts.
///
/// # Examples
///
/// Basic usage:
///
/// ```
/// use tincan::Scope;
///
/// let cx = Scope::new();
/// let count = cx.signal(0);
/// assert_eq!(count.get(), 0);
/// ```
///
/// Automatic cleanup:
///
/// ```
/// use tincan::Scope;
///
/// {
///     let cx = Scope::new();
///     let count = cx.signal(42);
///     // Use the signal...
/// } // Scope dropped, all signals cleaned up
/// ```
///
/// Nested scopes:
///
/// ```
/// use tincan::Scope;
///
/// let cx = Scope::new();
/// let outer = cx.signal(1);
///
/// {
///     let child_cx = Scope::new();
///     let inner = child_cx.signal(2);
///     assert_eq!(inner.get(), 2);
/// } // Child scope cleaned up
///
/// assert_eq!(outer.get(), 1);
/// ```
pub struct Scope {
    runtime: Rc<RefCell<RuntimeInner>>,
}

impl Scope {
    /// Create a new isolated reactive scope.
    ///
    /// Each scope has its own dependency graph and is completely independent
    /// from other scopes.
    ///
    /// # Examples
    ///
    /// ```
    /// use tincan::Scope;
    ///
    /// let cx = Scope::new();
    /// let count = cx.signal(0);
    /// ```
    pub fn new() -> Self {
        Self {
            runtime: Rc::new(RefCell::new(RuntimeInner::new())),
        }
    }

    /// Create a signal with the given initial value.
    ///
    /// Signals are the primary way to store reactive state. When a signal's
    /// value changes, all observers (effects and memos) are automatically notified.
    ///
    /// # Examples
    ///
    /// ```
    /// use tincan::Scope;
    ///
    /// let cx = Scope::new();
    /// let count = cx.signal(42);
    /// assert_eq!(count.get(), 42);
    ///
    /// count.set(100);
    /// assert_eq!(count.get(), 100);
    /// ```
    pub fn signal<T>(&self, value: T) -> Signal<T>
    where
        T: Clone + 'static,
    {
        let id = self.runtime.borrow().allocate_id();
        Signal::new_in_scope(value, id, Rc::clone(&self.runtime))
    }

    /// Create a computed value (memo) with the given function.
    ///
    /// Memos automatically track which signals they depend on and only
    /// recompute when those dependencies change.
    ///
    /// # Examples
    ///
    /// ```
    /// use tincan::Scope;
    ///
    /// let cx = Scope::new();
    /// let count = cx.signal(5);
    /// let doubled = cx.memo({
    ///     let count = count.clone();
    ///     move || count.get() * 2
    /// });
    ///
    /// assert_eq!(doubled.get(), 10);
    ///
    /// count.set(10);
    /// assert_eq!(doubled.get(), 20);
    /// ```
    pub fn memo<T, F>(&self, compute: F) -> Memo<T>
    where
        T: Clone + 'static,
        F: Fn() -> T + 'static,
    {
        let id = self.runtime.borrow().allocate_id();
        Memo::new_in_scope(compute, id, Rc::clone(&self.runtime))
    }

    /// Create an effect that runs when dependencies change.
    ///
    /// Effects automatically track which signals they read and re-run when
    /// any of those signals change. The effect runs immediately on creation.
    ///
    /// # Examples
    ///
    /// ```
    /// use tincan::Scope;
    /// use std::cell::Cell;
    /// use std::rc::Rc;
    ///
    /// let cx = Scope::new();
    /// let count = cx.signal(0);
    /// let run_count = Rc::new(Cell::new(0));
    ///
    /// let _effect = cx.effect({
    ///     let count = count.clone();
    ///     let run_count = Rc::clone(&run_count);
    ///     move || {
    ///         let _ = count.get();
    ///         run_count.set(run_count.get() + 1);
    ///     }
    /// });
    ///
    /// assert_eq!(run_count.get(), 1); // Runs immediately
    ///
    /// count.set(42);
    /// assert_eq!(run_count.get(), 2); // Runs on change
    /// ```
    pub fn effect<F>(&self, effect: F) -> Effect
    where
        F: Fn() + 'static,
    {
        let id = self.runtime.borrow().allocate_id();
        Effect::new_in_scope(effect, id, Rc::clone(&self.runtime))
    }

    /// Clear all reactive state in this scope.
    ///
    /// This removes all tracked dependencies and observers. Useful for
    /// testing or resetting a scope to its initial state.
    ///
    /// # Examples
    ///
    /// ```
    /// use tincan::Scope;
    ///
    /// let cx = Scope::new();
    /// let signal = cx.signal(42);
    /// cx.clear();
    /// // All state cleared
    /// ```
    pub fn clear(&self) {
        self.runtime.borrow().clear();
    }
}

impl Default for Scope {
    fn default() -> Self {
        Self::new()
    }
}