浏览代码

Merge pull request #1300 from Demonthos/signals

Complete Signals implementation
Jonathan Kelley 1 年之前
父节点
当前提交
c95f70f55a
共有 41 个文件被更改,包括 2827 次插入409 次删除
  1. 2 0
      Cargo.toml
  2. 1 7
      examples/clock.rs
  3. 1 1
      examples/signals.rs
  4. 13 4
      packages/core/src/arena.rs
  5. 5 5
      packages/core/src/create.rs
  6. 26 9
      packages/core/src/diff.rs
  7. 9 0
      packages/core/src/events.rs
  8. 14 7
      packages/core/src/lib.rs
  9. 108 0
      packages/core/src/runtime.rs
  10. 13 4
      packages/core/src/scheduler/wait.rs
  11. 33 26
      packages/core/src/scope_arena.rs
  12. 335 0
      packages/core/src/scope_context.rs
  13. 40 100
      packages/core/src/scopes.rs
  14. 38 20
      packages/core/src/virtual_dom.rs
  15. 7 2
      packages/desktop/src/protocol.rs
  16. 7 1
      packages/dioxus-tui/examples/hover.rs
  17. 17 0
      packages/generational-box/Cargo.toml
  18. 34 0
      packages/generational-box/README.md
  19. 359 0
      packages/generational-box/src/lib.rs
  20. 2 0
      packages/hooks/src/computed.rs
  21. 1 2
      packages/hooks/src/lib.rs
  22. 1 1
      packages/html/src/eval.rs
  23. 0 1
      packages/native-core/src/real_dom.rs
  24. 2 8
      packages/rsx/src/lib.rs
  25. 13 1
      packages/signals/Cargo.toml
  26. 122 0
      packages/signals/README.md
  27. 25 0
      packages/signals/examples/context.rs
  28. 38 0
      packages/signals/examples/dependancies.rs
  29. 30 0
      packages/signals/examples/selector.rs
  30. 150 0
      packages/signals/examples/split_subscriptions.rs
  31. 67 0
      packages/signals/src/dependency.rs
  32. 96 0
      packages/signals/src/effect.rs
  33. 294 0
      packages/signals/src/impls.rs
  34. 11 128
      packages/signals/src/lib.rs
  35. 120 82
      packages/signals/src/rt.rs
  36. 105 0
      packages/signals/src/selector.rs
  37. 344 0
      packages/signals/src/signal.rs
  38. 60 0
      packages/signals/tests/create.rs
  39. 47 0
      packages/signals/tests/effect.rs
  40. 145 0
      packages/signals/tests/selector.rs
  41. 92 0
      packages/signals/tests/subscribe.rs

+ 2 - 0
Cargo.toml

@@ -25,6 +25,7 @@ members = [
     "packages/native-core",
     "packages/native-core-macro",
     "packages/rsx-rosetta",
+    "packages/generational-box",
     "packages/signals",
     "packages/hot-reload",
     "packages/fullstack",
@@ -76,6 +77,7 @@ dioxus-native-core = { path = "packages/native-core", version = "0.4.0" }
 dioxus-native-core-macro = { path = "packages/native-core-macro", version = "0.4.0" }
 rsx-rosetta = { path = "packages/rsx-rosetta", version = "0.4.0" }
 dioxus-signals = { path = "packages/signals" }
+generational-box = { path = "packages/generational-box" }
 dioxus-hot-reload = { path = "packages/hot-reload", version = "0.4.0" }
 dioxus-fullstack = { path = "packages/fullstack", version = "0.4.1"  }
 dioxus_server_macro = { path = "packages/server-macro", version = "0.4.1" }

+ 1 - 7
examples/clock.rs

@@ -1,17 +1,11 @@
-//! Example: README.md showcase
-//!
-//! The example from the README.md.
-
 use dioxus::prelude::*;
-use dioxus_signals::{use_init_signal_rt, use_signal};
+use dioxus_signals::use_signal;
 
 fn main() {
     dioxus_desktop::launch(app);
 }
 
 fn app(cx: Scope) -> Element {
-    use_init_signal_rt(cx);
-
     let mut count = use_signal(cx, || 0);
 
     use_future!(cx, || async move {

+ 1 - 1
examples/signals.rs

@@ -20,7 +20,7 @@ fn app(cx: Scope) -> Element {
         button { onclick: move |_| count += 1, "Up high!" }
         button { onclick: move |_| count -= 1, "Down low!" }
 
-        if count() > 5 {
+        if count.value() > 5 {
             rsx!{ h2 { "High five!" } }
         }
     })

+ 13 - 4
packages/core/src/arena.rs

@@ -19,6 +19,9 @@ pub(crate) struct ElementRef {
 
     // The actual template
     pub template: Option<NonNull<VNode<'static>>>,
+
+    // The scope the element belongs to
+    pub scope: ScopeId,
 }
 
 #[derive(Clone, Copy, Debug)]
@@ -32,6 +35,7 @@ impl ElementRef {
         Self {
             template: None,
             path: ElementPath::Root(0),
+            scope: ScopeId(0),
         }
     }
 }
@@ -56,11 +60,13 @@ impl VirtualDom {
     fn next_reference(&mut self, template: &VNode, path: ElementPath) -> ElementId {
         let entry = self.elements.vacant_entry();
         let id = entry.key();
+        let scope = self.runtime.current_scope_id().unwrap_or(ScopeId(0));
 
         entry.insert(ElementRef {
             // We know this is non-null because it comes from a reference
             template: Some(unsafe { NonNull::new_unchecked(template as *const _ as *mut _) }),
             path,
+            scope,
         });
         ElementId(id)
     }
@@ -91,7 +97,7 @@ impl VirtualDom {
     // Note: This will not remove any ids from the arena
     pub(crate) fn drop_scope(&mut self, id: ScopeId, recursive: bool) {
         self.dirty_scopes.remove(&DirtyScope {
-            height: self.scopes[id.0].height,
+            height: self.scopes[id.0].height(),
             id,
         });
 
@@ -110,10 +116,13 @@ impl VirtualDom {
         // Drop all the hooks once the children are dropped
         // this means we'll drop hooks bottom-up
         scope.hooks.get_mut().clear();
+        {
+            let context = scope.context();
 
-        // Drop all the futures once the hooks are dropped
-        for task_id in scope.spawned_tasks.borrow_mut().drain() {
-            scope.tasks.remove(task_id);
+            // Drop all the futures once the hooks are dropped
+            for task_id in context.spawned_tasks.borrow_mut().drain() {
+                context.tasks.remove(task_id);
+            }
         }
 
         self.scopes.remove(id.0);

+ 5 - 5
packages/core/src/create.rs

@@ -66,10 +66,10 @@ impl<'b> VirtualDom {
     ///
     /// This method pushes the ScopeID to the internal scopestack and returns the number of nodes created.
     pub(crate) fn create_scope(&mut self, scope: ScopeId, template: &'b VNode<'b>) -> usize {
-        self.scope_stack.push(scope);
-        let out = self.create(template);
-        self.scope_stack.pop();
-        out
+        self.runtime.scope_stack.borrow_mut().push(scope);
+        let nodes = self.create(template);
+        self.runtime.scope_stack.borrow_mut().pop();
+        nodes
     }
 
     /// Create this template and write its mutations
@@ -522,7 +522,7 @@ impl<'b> VirtualDom {
             .take()
             .map(|props| {
                 let unbounded_props: Box<dyn AnyProps> = unsafe { std::mem::transmute(props) };
-                self.new_scope(unbounded_props, component.name).id
+                self.new_scope(unbounded_props, component.name).context().id
             })
             .unwrap_or_else(|| component.scope.get().unwrap())
     }

+ 26 - 9
packages/core/src/diff.rs

@@ -15,9 +15,7 @@ use DynamicNode::*;
 
 impl<'b> VirtualDom {
     pub(super) fn diff_scope(&mut self, scope: ScopeId) {
-        let scope_state = &mut self.scopes[scope.0];
-
-        self.scope_stack.push(scope);
+        let scope_state = &mut self.get_scope(scope).unwrap();
         unsafe {
             // Load the old and new bump arenas
             let old = scope_state
@@ -47,7 +45,6 @@ impl<'b> VirtualDom {
                 (Aborted(l), Ready(r)) => self.replace_placeholder(l, [r]),
             };
         }
-        self.scope_stack.pop();
     }
 
     fn diff_ok_to_err(&mut self, l: &'b VNode<'b>, p: &'b VPlaceholder) {
@@ -210,7 +207,7 @@ impl<'b> VirtualDom {
         self.diff_scope(scope_id);
 
         self.dirty_scopes.remove(&DirtyScope {
-            height: self.scopes[scope_id.0].height,
+            height: self.runtime.get_context(scope_id).unwrap().height,
             id: scope_id,
         });
     }
@@ -714,7 +711,12 @@ impl<'b> VirtualDom {
 
                     Component(comp) => {
                         let scope = comp.scope.get().unwrap();
-                        match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
+                        match unsafe {
+                            self.get_scope(scope)
+                                .unwrap()
+                                .root_node()
+                                .extend_lifetime_ref()
+                        } {
                             RenderReturn::Ready(node) => self.push_all_real_nodes(node),
                             RenderReturn::Aborted(_node) => todo!(),
                         }
@@ -915,7 +917,12 @@ impl<'b> VirtualDom {
             .expect("VComponents to always have a scope");
 
         // Remove the component from the dom
-        match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
+        match unsafe {
+            self.get_scope(scope)
+                .unwrap()
+                .root_node()
+                .extend_lifetime_ref()
+        } {
             RenderReturn::Ready(t) => self.remove_node(t, gen_muts),
             RenderReturn::Aborted(placeholder) => self.remove_placeholder(placeholder, gen_muts),
         };
@@ -936,7 +943,12 @@ impl<'b> VirtualDom {
             Some(Placeholder(t)) => t.id.get().unwrap(),
             Some(Component(comp)) => {
                 let scope = comp.scope.get().unwrap();
-                match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
+                match unsafe {
+                    self.get_scope(scope)
+                        .unwrap()
+                        .root_node()
+                        .extend_lifetime_ref()
+                } {
                     RenderReturn::Ready(t) => self.find_first_element(t),
                     _ => todo!("cannot handle nonstandard nodes"),
                 }
@@ -952,7 +964,12 @@ impl<'b> VirtualDom {
             Some(Placeholder(t)) => t.id.get().unwrap(),
             Some(Component(comp)) => {
                 let scope = comp.scope.get().unwrap();
-                match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
+                match unsafe {
+                    self.get_scope(scope)
+                        .unwrap()
+                        .root_node()
+                        .extend_lifetime_ref()
+                } {
                     RenderReturn::Ready(t) => self.find_last_element(t),
                     _ => todo!("cannot handle nonstandard nodes"),
                 }

+ 9 - 0
packages/core/src/events.rs

@@ -1,3 +1,4 @@
+use crate::{runtime::with_runtime, ScopeId};
 use std::{
     cell::{Cell, RefCell},
     rc::Rc,
@@ -135,12 +136,14 @@ impl<T: std::fmt::Debug> std::fmt::Debug for Event<T> {
 ///
 /// ```
 pub struct EventHandler<'bump, T = ()> {
+    pub(crate) origin: ScopeId,
     pub(super) callback: RefCell<Option<ExternalListenerCallback<'bump, T>>>,
 }
 
 impl<T> Default for EventHandler<'_, T> {
     fn default() -> Self {
         Self {
+            origin: ScopeId(0),
             callback: Default::default(),
         }
     }
@@ -154,7 +157,13 @@ impl<T> EventHandler<'_, T> {
     /// This borrows the event using a RefCell. Recursively calling a listener will cause a panic.
     pub fn call(&self, event: T) {
         if let Some(callback) = self.callback.borrow_mut().as_mut() {
+            with_runtime(|rt| {
+                rt.scope_stack.borrow_mut().push(self.origin);
+            });
             callback(event);
+            with_runtime(|rt| {
+                rt.scope_stack.borrow_mut().pop();
+            });
         }
     }
 

+ 14 - 7
packages/core/src/lib.rs

@@ -14,8 +14,10 @@ mod lazynodes;
 mod mutations;
 mod nodes;
 mod properties;
+mod runtime;
 mod scheduler;
 mod scope_arena;
+mod scope_context;
 mod scopes;
 mod virtual_dom;
 
@@ -31,6 +33,7 @@ pub(crate) mod innerlude {
     pub use crate::nodes::*;
     pub use crate::properties::*;
     pub use crate::scheduler::*;
+    pub use crate::scope_context::*;
     pub use crate::scopes::*;
     pub use crate::virtual_dom::*;
 
@@ -70,10 +73,11 @@ pub(crate) mod innerlude {
 }
 
 pub use crate::innerlude::{
-    fc_to_builder, AnyValue, Attribute, AttributeValue, BorrowedAttributeValue, CapturedError,
-    Component, DynamicNode, Element, ElementId, Event, Fragment, IntoDynNode, LazyNodes, Mutation,
-    Mutations, Properties, RenderReturn, Scope, ScopeId, ScopeState, Scoped, TaskId, Template,
-    TemplateAttribute, TemplateNode, VComponent, VNode, VPlaceholder, VText, VirtualDom,
+    fc_to_builder, vdom_is_rendering, AnyValue, Attribute, AttributeValue, BorrowedAttributeValue,
+    CapturedError, Component, DynamicNode, Element, ElementId, Event, Fragment, IntoDynNode,
+    LazyNodes, Mutation, Mutations, Properties, RenderReturn, Scope, ScopeId, ScopeState, Scoped,
+    TaskId, Template, TemplateAttribute, TemplateNode, VComponent, VNode, VPlaceholder, VText,
+    VirtualDom,
 };
 
 /// The purpose of this module is to alleviate imports of many common types
@@ -81,9 +85,12 @@ pub use crate::innerlude::{
 /// This includes types like [`Scope`], [`Element`], and [`Component`].
 pub mod prelude {
     pub use crate::innerlude::{
-        fc_to_builder, AnyValue, Component, Element, Event, EventHandler, Fragment,
-        IntoAttributeValue, LazyNodes, Properties, Scope, ScopeId, ScopeState, Scoped, TaskId,
-        Template, TemplateAttribute, TemplateNode, Throw, VNode, VirtualDom,
+        consume_context, consume_context_from_scope, current_scope_id, fc_to_builder, has_context,
+        provide_context, provide_context_to_scope, provide_root_context, push_future,
+        remove_future, schedule_update_any, spawn, spawn_forever, suspend, throw, AnyValue,
+        Component, Element, Event, EventHandler, Fragment, IntoAttributeValue, LazyNodes,
+        Properties, Scope, ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute,
+        TemplateNode, Throw, VNode, VirtualDom,
     };
 }
 

+ 108 - 0
packages/core/src/runtime.rs

@@ -0,0 +1,108 @@
+use std::cell::{Cell, Ref, RefCell};
+
+use crate::{innerlude::Scheduler, scope_context::ScopeContext, scopes::ScopeId};
+use std::rc::Rc;
+
+thread_local! {
+    static RUNTIMES: RefCell<Vec<Rc<Runtime>>> = RefCell::new(vec![]);
+}
+
+/// Pushes a new scope onto the stack
+pub(crate) fn push_runtime(runtime: Rc<Runtime>) {
+    RUNTIMES.with(|stack| stack.borrow_mut().push(runtime));
+}
+
+/// Pops a scope off the stack
+pub(crate) fn pop_runtime() {
+    RUNTIMES.with(|stack| stack.borrow_mut().pop());
+}
+
+/// Runs a function with the current runtime
+pub(crate) fn with_runtime<F, R>(f: F) -> Option<R>
+where
+    F: FnOnce(&Runtime) -> R,
+{
+    RUNTIMES.with(|stack| {
+        let stack = stack.borrow();
+        stack.last().map(|r| f(r))
+    })
+}
+
+/// Runs a function with the current scope
+pub(crate) fn with_current_scope<F, R>(f: F) -> Option<R>
+where
+    F: FnOnce(&ScopeContext) -> R,
+{
+    with_runtime(|runtime| {
+        runtime
+            .current_scope_id()
+            .and_then(|scope| runtime.get_context(scope).map(|sc| f(&sc)))
+    })
+    .flatten()
+}
+
+pub(crate) struct Runtime {
+    pub(crate) scope_contexts: RefCell<Vec<Option<ScopeContext>>>,
+    pub(crate) scheduler: Rc<Scheduler>,
+
+    // We use this to track the current scope
+    pub(crate) scope_stack: RefCell<Vec<ScopeId>>,
+    pub(crate) rendering: Cell<bool>,
+}
+
+impl Runtime {
+    pub(crate) fn new(scheduler: Rc<Scheduler>) -> Rc<Self> {
+        Rc::new(Self {
+            scheduler,
+
+            scope_contexts: Default::default(),
+
+            scope_stack: Default::default(),
+
+            rendering: Cell::new(true),
+        })
+    }
+
+    /// Create a scope context. This slab is synchronized with the scope slab.
+    pub(crate) fn create_context_at(&self, id: ScopeId, context: ScopeContext) {
+        let mut contexts = self.scope_contexts.borrow_mut();
+        if contexts.len() <= id.0 {
+            contexts.resize_with(id.0 + 1, Default::default);
+        }
+        contexts[id.0] = Some(context);
+    }
+
+    pub(crate) fn remove_context(&self, id: ScopeId) {
+        self.scope_contexts.borrow_mut()[id.0] = None;
+    }
+
+    /// Get the current scope id
+    pub fn current_scope_id(&self) -> Option<ScopeId> {
+        self.scope_stack.borrow().last().copied()
+    }
+
+    /// Get the context for any scope given its ID
+    ///
+    /// This is useful for inserting or removing contexts from a scope, or rendering out its root node
+    pub fn get_context(&self, id: ScopeId) -> Option<Ref<'_, ScopeContext>> {
+        Ref::filter_map(self.scope_contexts.borrow(), |contexts| {
+            contexts.get(id.0).and_then(|f| f.as_ref())
+        })
+        .ok()
+    }
+}
+
+pub(crate) struct RuntimeGuard(Rc<Runtime>);
+
+impl RuntimeGuard {
+    pub(crate) fn new(runtime: Rc<Runtime>) -> Self {
+        push_runtime(runtime.clone());
+        Self(runtime)
+    }
+}
+
+impl Drop for RuntimeGuard {
+    fn drop(&mut self) {
+        pop_runtime();
+    }
+}

+ 13 - 4
packages/core/src/scheduler/wait.rs

@@ -1,4 +1,4 @@
-use crate::{TaskId, VirtualDom};
+use crate::{runtime::RuntimeGuard, TaskId, VirtualDom};
 use std::task::Context;
 
 impl VirtualDom {
@@ -7,7 +7,8 @@ impl VirtualDom {
     /// This is precise, meaning we won't poll every task, just tasks that have woken up as notified to use by the
     /// queue
     pub(crate) fn handle_task_wakeup(&mut self, id: TaskId) {
-        let mut tasks = self.scheduler.tasks.borrow_mut();
+        let _runtime = RuntimeGuard::new(self.runtime.clone());
+        let mut tasks = self.runtime.scheduler.tasks.borrow_mut();
 
         let task = match tasks.get(id.0) {
             Some(task) => task,
@@ -17,14 +18,22 @@ impl VirtualDom {
 
         let mut cx = Context::from_waker(&task.waker);
 
+        // update the scope stack
+        self.runtime.scope_stack.borrow_mut().push(task.scope);
+        self.runtime.rendering.set(false);
+
         // If the task completes...
         if task.task.borrow_mut().as_mut().poll(&mut cx).is_ready() {
             // Remove it from the scope so we dont try to double drop it when the scope dropes
-            let scope = &self.scopes[task.scope.0];
-            scope.spawned_tasks.borrow_mut().remove(&id);
+            let scope = &self.get_scope(task.scope).unwrap();
+            scope.context().spawned_tasks.borrow_mut().remove(&id);
 
             // Remove it from the scheduler
             tasks.try_remove(id.0);
         }
+
+        // Remove the scope from the stack
+        self.runtime.scope_stack.borrow_mut().pop();
+        self.runtime.rendering.set(true);
     }
 }

+ 33 - 26
packages/core/src/scope_arena.rs

@@ -3,6 +3,7 @@ use crate::{
     bump_frame::BumpFrame,
     innerlude::DirtyScope,
     nodes::RenderReturn,
+    scope_context::ScopeContext,
     scopes::{ScopeId, ScopeState},
     virtual_dom::VirtualDom,
 };
@@ -13,48 +14,49 @@ impl VirtualDom {
         props: Box<dyn AnyProps<'static>>,
         name: &'static str,
     ) -> &ScopeState {
-        let parent = self.acquire_current_scope_raw();
+        let parent_id = self.runtime.current_scope_id();
+        let height = parent_id
+            .and_then(|parent_id| self.get_scope(parent_id).map(|f| f.context().height + 1))
+            .unwrap_or(0);
         let entry = self.scopes.vacant_entry();
-        let height = unsafe { parent.map(|f| (*f).height + 1).unwrap_or(0) };
         let id = ScopeId(entry.key());
 
-        entry.insert(Box::new(ScopeState {
-            parent,
-            id,
-            height,
-            name,
+        let scope = entry.insert(Box::new(ScopeState {
+            runtime: self.runtime.clone(),
+            context_id: id,
+
             props: Some(props),
-            tasks: self.scheduler.clone(),
+
             node_arena_1: BumpFrame::new(0),
             node_arena_2: BumpFrame::new(0),
-            spawned_tasks: Default::default(),
-            suspended: Default::default(),
+
             render_cnt: Default::default(),
             hooks: Default::default(),
             hook_idx: Default::default(),
-            shared_contexts: Default::default(),
+
             borrowed_props: Default::default(),
             attributes_to_drop: Default::default(),
-        }))
-    }
+        }));
 
-    fn acquire_current_scope_raw(&self) -> Option<*const ScopeState> {
-        let id = self.scope_stack.last().copied()?;
-        let scope = self.scopes.get(id.0)?;
-        Some(scope.as_ref())
+        let context =
+            ScopeContext::new(name, id, parent_id, height, self.runtime.scheduler.clone());
+        self.runtime.create_context_at(id, context);
+
+        scope
     }
 
     pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> &RenderReturn {
+        self.runtime.scope_stack.borrow_mut().push(scope_id);
         // Cycle to the next frame and then reset it
         // This breaks any latent references, invalidating every pointer referencing into it.
         // Remove all the outdated listeners
         self.ensure_drop_safety(scope_id);
 
         let new_nodes = unsafe {
-            self.scopes[scope_id.0].previous_frame().bump_mut().reset();
-
             let scope = &self.scopes[scope_id.0];
-            scope.suspended.set(false);
+            scope.previous_frame().bump_mut().reset();
+
+            scope.context().suspended.set(false);
 
             scope.hook_idx.set(0);
 
@@ -77,21 +79,26 @@ impl VirtualDom {
         // And move the render generation forward by one
         scope.render_cnt.set(scope.render_cnt.get() + 1);
 
+        let context = scope.context();
         // remove this scope from dirty scopes
         self.dirty_scopes.remove(&DirtyScope {
-            height: scope.height,
-            id: scope.id,
+            height: context.height,
+            id: context.id,
         });
 
-        if scope.suspended.get() {
+        if context.suspended.get() {
             if matches!(allocated, RenderReturn::Aborted(_)) {
-                self.suspended_scopes.insert(scope.id);
+                self.suspended_scopes.insert(context.id);
             }
         } else if !self.suspended_scopes.is_empty() {
-            _ = self.suspended_scopes.remove(&scope.id);
+            _ = self.suspended_scopes.remove(&context.id);
         }
 
         // rebind the lifetime now that its stored internally
-        unsafe { allocated.extend_lifetime_ref() }
+        let result = unsafe { allocated.extend_lifetime_ref() };
+
+        self.runtime.scope_stack.borrow_mut().pop();
+
+        result
     }
 }

+ 335 - 0
packages/core/src/scope_context.rs

@@ -0,0 +1,335 @@
+use crate::{
+    innerlude::{ErrorBoundary, Scheduler, SchedulerMsg},
+    runtime::{with_current_scope, with_runtime},
+    Element, ScopeId, TaskId,
+};
+use rustc_hash::FxHashSet;
+use std::{
+    any::Any,
+    cell::{Cell, RefCell},
+    fmt::Debug,
+    future::Future,
+    rc::Rc,
+    sync::Arc,
+};
+
+/// A component's state separate from its props.
+///
+/// This struct exists to provide a common interface for all scopes without relying on generics.
+pub(crate) struct ScopeContext {
+    pub(crate) name: &'static str,
+
+    pub(crate) id: ScopeId,
+    pub(crate) parent_id: Option<ScopeId>,
+
+    pub(crate) height: u32,
+    pub(crate) suspended: Cell<bool>,
+
+    pub(crate) shared_contexts: RefCell<Vec<Box<dyn Any>>>,
+
+    pub(crate) tasks: Rc<Scheduler>,
+    pub(crate) spawned_tasks: RefCell<FxHashSet<TaskId>>,
+}
+
+impl ScopeContext {
+    pub(crate) fn new(
+        name: &'static str,
+        id: ScopeId,
+        parent_id: Option<ScopeId>,
+        height: u32,
+        tasks: Rc<Scheduler>,
+    ) -> Self {
+        Self {
+            name,
+            id,
+            parent_id,
+            height,
+            suspended: Cell::new(false),
+            shared_contexts: RefCell::new(vec![]),
+            tasks,
+            spawned_tasks: RefCell::new(FxHashSet::default()),
+        }
+    }
+
+    pub fn parent_id(&self) -> Option<ScopeId> {
+        self.parent_id
+    }
+
+    pub fn scope_id(&self) -> ScopeId {
+        self.id
+    }
+
+    /// Create a subscription that schedules a future render for the reference component
+    ///
+    /// ## Notice: you should prefer using [`Self::schedule_update_any`] and [`Self::scope_id`]
+    pub fn schedule_update(&self) -> Arc<dyn Fn() + Send + Sync + 'static> {
+        let (chan, id) = (self.tasks.sender.clone(), self.scope_id());
+        Arc::new(move || drop(chan.unbounded_send(SchedulerMsg::Immediate(id))))
+    }
+
+    /// Schedule an update for any component given its [`ScopeId`].
+    ///
+    /// A component's [`ScopeId`] can be obtained from `use_hook` or the [`ScopeState::scope_id`] method.
+    ///
+    /// This method should be used when you want to schedule an update for a component
+    pub fn schedule_update_any(&self) -> Arc<dyn Fn(ScopeId) + Send + Sync> {
+        let chan = self.tasks.sender.clone();
+        Arc::new(move |id| {
+            chan.unbounded_send(SchedulerMsg::Immediate(id)).unwrap();
+        })
+    }
+
+    /// Mark this scope as dirty, and schedule a render for it.
+    pub fn needs_update(&self) {
+        self.needs_update_any(self.scope_id());
+    }
+
+    /// Get the [`ScopeId`] of a mounted component.
+    ///
+    /// `ScopeId` is not unique for the lifetime of the [`crate::VirtualDom`] - a [`ScopeId`] will be reused if a component is unmounted.
+    pub fn needs_update_any(&self, id: ScopeId) {
+        self.tasks
+            .sender
+            .unbounded_send(SchedulerMsg::Immediate(id))
+            .expect("Scheduler to exist if scope exists");
+    }
+
+    /// Return any context of type T if it exists on this scope
+    pub fn has_context<T: 'static + Clone>(&self) -> Option<T> {
+        self.shared_contexts
+            .borrow()
+            .iter()
+            .find_map(|any| any.downcast_ref::<T>())
+            .cloned()
+    }
+
+    /// Try to retrieve a shared state with type `T` from any parent scope.
+    ///
+    /// Clones the state if it exists.
+    pub fn consume_context<T: 'static + Clone>(&self) -> Option<T> {
+        if let Some(this_ctx) = self.has_context() {
+            return Some(this_ctx);
+        }
+
+        let mut search_parent = self.parent_id;
+        with_runtime(|runtime| {
+            while let Some(parent_id) = search_parent {
+                let parent = runtime.get_context(parent_id).unwrap();
+                if let Some(shared) = parent
+                    .shared_contexts
+                    .borrow()
+                    .iter()
+                    .find_map(|any| any.downcast_ref::<T>())
+                {
+                    return Some(shared.clone());
+                }
+                search_parent = parent.parent_id;
+            }
+            None
+        })
+        .flatten()
+    }
+
+    /// Expose state to children further down the [`crate::VirtualDom`] Tree. Requires `Clone` on the context to allow getting values down the tree.
+    ///
+    /// This is a "fundamental" operation and should only be called during initialization of a hook.
+    ///
+    /// For a hook that provides the same functionality, use `use_provide_context` and `use_context` instead.
+    ///
+    /// # Example
+    ///
+    /// ```rust, ignore
+    /// struct SharedState(&'static str);
+    ///
+    /// static App: Component = |cx| {
+    ///     cx.use_hook(|| cx.provide_context(SharedState("world")));
+    ///     render!(Child {})
+    /// }
+    ///
+    /// static Child: Component = |cx| {
+    ///     let state = cx.consume_state::<SharedState>();
+    ///     render!(div { "hello {state.0}" })
+    /// }
+    /// ```
+    pub fn provide_context<T: 'static + Clone>(&self, value: T) -> T {
+        let mut contexts = self.shared_contexts.borrow_mut();
+
+        // If the context exists, swap it out for the new value
+        for ctx in contexts.iter_mut() {
+            // Swap the ptr directly
+            if let Some(ctx) = ctx.downcast_mut::<T>() {
+                std::mem::swap(ctx, &mut value.clone());
+                return value;
+            }
+        }
+
+        // Else, just push it
+        contexts.push(Box::new(value.clone()));
+
+        value
+    }
+
+    /// Provide a context to the root and then consume it
+    ///
+    /// This is intended for "global" state management solutions that would rather be implicit for the entire app.
+    /// Things like signal runtimes and routers are examples of "singletons" that would benefit from lazy initialization.
+    ///
+    /// Note that you should be checking if the context existed before trying to provide a new one. Providing a context
+    /// when a context already exists will swap the context out for the new one, which may not be what you want.
+    pub fn provide_root_context<T: 'static + Clone>(&self, context: T) -> T {
+        with_runtime(|runtime| {
+            runtime
+                .get_context(ScopeId(0))
+                .unwrap()
+                .provide_context(context)
+        })
+        .expect("Runtime to exist")
+    }
+
+    /// Pushes the future onto the poll queue to be polled after the component renders.
+    pub fn push_future(&self, fut: impl Future<Output = ()> + 'static) -> TaskId {
+        let id = self.tasks.spawn(self.id, fut);
+        self.spawned_tasks.borrow_mut().insert(id);
+        id
+    }
+
+    /// Spawns the future but does not return the [`TaskId`]
+    pub fn spawn(&self, fut: impl Future<Output = ()> + 'static) {
+        self.push_future(fut);
+    }
+
+    /// Spawn a future that Dioxus won't clean up when this component is unmounted
+    ///
+    /// This is good for tasks that need to be run after the component has been dropped.
+    pub fn spawn_forever(&self, fut: impl Future<Output = ()> + 'static) -> TaskId {
+        // The root scope will never be unmounted so we can just add the task at the top of the app
+        let id = self.tasks.spawn(ScopeId(0), fut);
+
+        // wake up the scheduler if it is sleeping
+        self.tasks
+            .sender
+            .unbounded_send(SchedulerMsg::TaskNotified(id))
+            .expect("Scheduler should exist");
+
+        self.spawned_tasks.borrow_mut().insert(id);
+
+        id
+    }
+
+    /// Informs the scheduler that this task is no longer needed and should be removed.
+    ///
+    /// This drops the task immediately.
+    pub fn remove_future(&self, id: TaskId) {
+        self.tasks.remove(id);
+    }
+
+    /// Inject an error into the nearest error boundary and quit rendering
+    ///
+    /// The error doesn't need to implement Error or any specific traits since the boundary
+    /// itself will downcast the error into a trait object.
+    pub fn throw(&self, error: impl Debug + 'static) -> Option<()> {
+        if let Some(cx) = self.consume_context::<Rc<ErrorBoundary>>() {
+            cx.insert_error(self.scope_id(), Box::new(error));
+        }
+
+        // Always return none during a throw
+        None
+    }
+
+    /// Mark this component as suspended and then return None
+    pub fn suspend(&self) -> Option<Element> {
+        self.suspended.set(true);
+        None
+    }
+}
+
+/// Schedule an update for any component given its [`ScopeId`].
+///
+/// A component's [`ScopeId`] can be obtained from `use_hook` or the [`crate::scopes::ScopeState::scope_id`] method.
+///
+/// This method should be used when you want to schedule an update for a component
+pub fn schedule_update_any() -> Option<Arc<dyn Fn(ScopeId) + Send + Sync>> {
+    with_current_scope(|cx| cx.schedule_update_any())
+}
+
+/// Get the current scope id
+pub fn current_scope_id() -> Option<ScopeId> {
+    with_runtime(|rt| rt.current_scope_id()).flatten()
+}
+
+#[doc(hidden)]
+/// Check if the virtual dom is currently inside of the body of a component
+pub fn vdom_is_rendering() -> bool {
+    with_runtime(|rt| rt.rendering.get()).unwrap_or_default()
+}
+
+/// Consume context from the current scope
+pub fn consume_context<T: 'static + Clone>() -> Option<T> {
+    with_current_scope(|cx| cx.consume_context::<T>()).flatten()
+}
+
+/// Consume context from the current scope
+pub fn consume_context_from_scope<T: 'static + Clone>(scope_id: ScopeId) -> Option<T> {
+    with_runtime(|rt| {
+        rt.get_context(scope_id)
+            .and_then(|cx| cx.consume_context::<T>())
+    })
+    .flatten()
+}
+
+/// Check if the current scope has a context
+pub fn has_context<T: 'static + Clone>() -> Option<T> {
+    with_current_scope(|cx| cx.has_context::<T>()).flatten()
+}
+
+/// Provide context to the current scope
+pub fn provide_context<T: 'static + Clone>(value: T) -> Option<T> {
+    with_current_scope(|cx| cx.provide_context(value))
+}
+
+/// Provide context to the the given scope
+pub fn provide_context_to_scope<T: 'static + Clone>(scope_id: ScopeId, value: T) -> Option<T> {
+    with_runtime(|rt| rt.get_context(scope_id).map(|cx| cx.provide_context(value))).flatten()
+}
+
+/// Provide a context to the root scope
+pub fn provide_root_context<T: 'static + Clone>(value: T) -> Option<T> {
+    with_current_scope(|cx| cx.provide_root_context(value))
+}
+
+/// Suspends the current component
+pub fn suspend() -> Option<Element<'static>> {
+    with_current_scope(|cx| {
+        cx.suspend();
+    });
+    None
+}
+
+/// Throw an error into the nearest error boundary
+pub fn throw(error: impl Debug + 'static) -> Option<()> {
+    with_current_scope(|cx| cx.throw(error)).flatten()
+}
+
+/// Pushes the future onto the poll queue to be polled after the component renders.
+pub fn push_future(fut: impl Future<Output = ()> + 'static) -> Option<TaskId> {
+    with_current_scope(|cx| cx.push_future(fut))
+}
+
+/// Spawns the future but does not return the [`TaskId`]
+pub fn spawn(fut: impl Future<Output = ()> + 'static) {
+    with_current_scope(|cx| cx.spawn(fut));
+}
+
+/// Spawn a future that Dioxus won't clean up when this component is unmounted
+///
+/// This is good for tasks that need to be run after the component has been dropped.
+pub fn spawn_forever(fut: impl Future<Output = ()> + 'static) -> Option<TaskId> {
+    with_current_scope(|cx| cx.spawn_forever(fut))
+}
+
+/// Informs the scheduler that this task is no longer needed and should be removed.
+///
+/// This drops the task immediately.
+pub fn remove_future(id: TaskId) {
+    with_current_scope(|cx| cx.remove_future(id));
+}

+ 40 - 100
packages/core/src/scopes.rs

@@ -2,17 +2,18 @@ use crate::{
     any_props::AnyProps,
     any_props::VProps,
     bump_frame::BumpFrame,
+    innerlude::ErrorBoundary,
     innerlude::{DynamicNode, EventHandler, VComponent, VText},
-    innerlude::{ErrorBoundary, Scheduler, SchedulerMsg},
     lazynodes::LazyNodes,
     nodes::{IntoAttributeValue, IntoDynNode, RenderReturn},
+    runtime::Runtime,
+    scope_context::ScopeContext,
     AnyValue, Attribute, AttributeValue, Element, Event, Properties, TaskId,
 };
 use bumpalo::{boxed::Box as BumpBox, Bump};
-use rustc_hash::FxHashSet;
 use std::{
-    any::{Any, TypeId},
-    cell::{Cell, RefCell, UnsafeCell},
+    any::Any,
+    cell::{Cell, Ref, RefCell, UnsafeCell},
     fmt::{Arguments, Debug},
     future::Future,
     rc::Rc,
@@ -66,33 +67,34 @@ pub struct ScopeId(pub usize);
 ///
 /// This struct exists to provide a common interface for all scopes without relying on generics.
 pub struct ScopeState {
+    pub(crate) runtime: Rc<Runtime>,
+    pub(crate) context_id: ScopeId,
+
     pub(crate) render_cnt: Cell<usize>,
-    pub(crate) name: &'static str,
 
     pub(crate) node_arena_1: BumpFrame,
     pub(crate) node_arena_2: BumpFrame,
 
-    pub(crate) parent: Option<*const ScopeState>,
-    pub(crate) id: ScopeId,
-
-    pub(crate) height: u32,
-    pub(crate) suspended: Cell<bool>,
-
     pub(crate) hooks: RefCell<Vec<Box<UnsafeCell<dyn Any>>>>,
     pub(crate) hook_idx: Cell<usize>,
 
-    pub(crate) shared_contexts: RefCell<Vec<(TypeId, Box<dyn Any>)>>,
-
-    pub(crate) tasks: Rc<Scheduler>,
-    pub(crate) spawned_tasks: RefCell<FxHashSet<TaskId>>,
-
     pub(crate) borrowed_props: RefCell<Vec<*const VComponent<'static>>>,
     pub(crate) attributes_to_drop: RefCell<Vec<*const Attribute<'static>>>,
 
     pub(crate) props: Option<Box<dyn AnyProps<'static>>>,
 }
 
+impl Drop for ScopeState {
+    fn drop(&mut self) {
+        self.runtime.remove_context(self.context_id);
+    }
+}
+
 impl<'src> ScopeState {
+    pub(crate) fn context(&self) -> Ref<'_, ScopeContext> {
+        self.runtime.get_context(self.context_id).unwrap()
+    }
+
     pub(crate) fn current_frame(&self) -> &BumpFrame {
         match self.render_cnt.get() % 2 {
             0 => &self.node_arena_1,
@@ -111,7 +113,7 @@ impl<'src> ScopeState {
 
     /// Get the name of this component
     pub fn name(&self) -> &str {
-        self.name
+        self.context().name
     }
 
     /// Get the current render since the inception of this component
@@ -174,7 +176,7 @@ impl<'src> ScopeState {
     /// assert_eq!(base.height(), 0);
     /// ```
     pub fn height(&self) -> u32 {
-        self.height
+        self.context().height
     }
 
     /// Get the Parent of this [`Scope`] within this Dioxus [`crate::VirtualDom`].
@@ -195,7 +197,7 @@ impl<'src> ScopeState {
     /// ```
     pub fn parent(&self) -> Option<ScopeId> {
         // safety: the pointer to our parent is *always* valid thanks to the bump arena
-        self.parent.map(|p| unsafe { &*p }.id)
+        self.context().parent_id()
     }
 
     /// Get the ID of this Scope within this Dioxus [`crate::VirtualDom`].
@@ -212,15 +214,14 @@ impl<'src> ScopeState {
     /// assert_eq!(base.scope_id(), 0);
     /// ```
     pub fn scope_id(&self) -> ScopeId {
-        self.id
+        self.context().scope_id()
     }
 
     /// Create a subscription that schedules a future render for the reference component
     ///
     /// ## Notice: you should prefer using [`Self::schedule_update_any`] and [`Self::scope_id`]
     pub fn schedule_update(&self) -> Arc<dyn Fn() + Send + Sync + 'static> {
-        let (chan, id) = (self.tasks.sender.clone(), self.scope_id());
-        Arc::new(move || drop(chan.unbounded_send(SchedulerMsg::Immediate(id))))
+        self.context().schedule_update()
     }
 
     /// Schedule an update for any component given its [`ScopeId`].
@@ -229,61 +230,31 @@ impl<'src> ScopeState {
     ///
     /// This method should be used when you want to schedule an update for a component
     pub fn schedule_update_any(&self) -> Arc<dyn Fn(ScopeId) + Send + Sync> {
-        let chan = self.tasks.sender.clone();
-        Arc::new(move |id| {
-            chan.unbounded_send(SchedulerMsg::Immediate(id)).unwrap();
-        })
+        self.context().schedule_update_any()
     }
 
     /// Mark this scope as dirty, and schedule a render for it.
     pub fn needs_update(&self) {
-        self.needs_update_any(self.scope_id());
+        self.context().needs_update()
     }
 
     /// Get the [`ScopeId`] of a mounted component.
     ///
     /// `ScopeId` is not unique for the lifetime of the [`crate::VirtualDom`] - a [`ScopeId`] will be reused if a component is unmounted.
     pub fn needs_update_any(&self, id: ScopeId) {
-        self.tasks
-            .sender
-            .unbounded_send(SchedulerMsg::Immediate(id))
-            .expect("Scheduler to exist if scope exists");
+        self.context().needs_update_any(id)
     }
 
     /// Return any context of type T if it exists on this scope
     pub fn has_context<T: 'static + Clone>(&self) -> Option<T> {
-        self.shared_contexts
-            .borrow()
-            .iter()
-            .find(|(k, _)| *k == TypeId::of::<T>())
-            .map(|(_, v)| v)?
-            .downcast_ref::<T>()
-            .cloned()
+        self.context().has_context()
     }
 
     /// Try to retrieve a shared state with type `T` from any parent scope.
     ///
     /// Clones the state if it exists.
     pub fn consume_context<T: 'static + Clone>(&self) -> Option<T> {
-        if let Some(this_ctx) = self.has_context() {
-            return Some(this_ctx);
-        }
-
-        let mut search_parent = self.parent;
-        while let Some(parent_ptr) = search_parent {
-            // safety: all parent pointers are valid thanks to the bump arena
-            let parent = unsafe { &*parent_ptr };
-            if let Some(shared) = parent
-                .shared_contexts
-                .borrow()
-                .iter()
-                .find(|(k, _)| *k == TypeId::of::<T>())
-            {
-                return shared.1.downcast_ref::<T>().cloned();
-            }
-            search_parent = parent.parent;
-        }
-        None
+        self.context().consume_context()
     }
 
     /// Expose state to children further down the [`crate::VirtualDom`] Tree. Requires `Clone` on the context to allow getting values down the tree.
@@ -308,21 +279,7 @@ impl<'src> ScopeState {
     /// }
     /// ```
     pub fn provide_context<T: 'static + Clone>(&self, value: T) -> T {
-        let mut contexts = self.shared_contexts.borrow_mut();
-
-        // If the context exists, swap it out for the new value
-        for ctx in contexts.iter_mut() {
-            // Swap the ptr directly
-            if let Some(ctx) = ctx.1.downcast_mut::<T>() {
-                std::mem::swap(ctx, &mut value.clone());
-                return value;
-            }
-        }
-
-        // Else, just push it
-        contexts.push((TypeId::of::<T>(), Box::new(value.clone())));
-
-        value
+        self.context().provide_context(value)
     }
 
     /// Provide a context to the root and then consume it
@@ -333,52 +290,31 @@ impl<'src> ScopeState {
     /// Note that you should be checking if the context existed before trying to provide a new one. Providing a context
     /// when a context already exists will swap the context out for the new one, which may not be what you want.
     pub fn provide_root_context<T: 'static + Clone>(&self, context: T) -> T {
-        let mut parent = self;
-
-        // Walk upwards until there is no more parent - and tada we have the root
-        while let Some(next_parent) = parent.parent {
-            parent = unsafe { &*next_parent };
-            debug_assert_eq!(parent.scope_id(), ScopeId(0));
-        }
-
-        parent.provide_context(context)
+        self.context().provide_root_context(context)
     }
 
     /// Pushes the future onto the poll queue to be polled after the component renders.
     pub fn push_future(&self, fut: impl Future<Output = ()> + 'static) -> TaskId {
-        let id = self.tasks.spawn(self.id, fut);
-        self.spawned_tasks.borrow_mut().insert(id);
-        id
+        self.context().push_future(fut)
     }
 
     /// Spawns the future but does not return the [`TaskId`]
     pub fn spawn(&self, fut: impl Future<Output = ()> + 'static) {
-        self.push_future(fut);
+        self.context().spawn(fut);
     }
 
     /// Spawn a future that Dioxus won't clean up when this component is unmounted
     ///
     /// This is good for tasks that need to be run after the component has been dropped.
     pub fn spawn_forever(&self, fut: impl Future<Output = ()> + 'static) -> TaskId {
-        // The root scope will never be unmounted so we can just add the task at the top of the app
-        let id = self.tasks.spawn(ScopeId(0), fut);
-
-        // wake up the scheduler if it is sleeping
-        self.tasks
-            .sender
-            .unbounded_send(SchedulerMsg::TaskNotified(id))
-            .expect("Scheduler should exist");
-
-        self.spawned_tasks.borrow_mut().insert(id);
-
-        id
+        self.context().spawn_forever(fut)
     }
 
     /// Informs the scheduler that this task is no longer needed and should be removed.
     ///
     /// This drops the task immediately.
     pub fn remove_future(&self, id: TaskId) {
-        self.tasks.remove(id);
+        self.context().remove_future(id);
     }
 
     /// Take a lazy [`crate::VNode`] structure and actually build it with the context of the efficient [`bumpalo::Bump`] allocator.
@@ -511,7 +447,10 @@ impl<'src> ScopeState {
         let handler: &mut dyn FnMut(T) = self.bump().alloc(f);
         let caller = unsafe { BumpBox::from_raw(handler as *mut dyn FnMut(T)) };
         let callback = RefCell::new(Some(caller));
-        EventHandler { callback }
+        EventHandler {
+            callback,
+            origin: self.context().id,
+        }
     }
 
     /// Create a new [`AttributeValue`] with the listener variant from a callback
@@ -565,7 +504,8 @@ impl<'src> ScopeState {
 
     /// Mark this component as suspended and then return None
     pub fn suspend(&self) -> Option<Element> {
-        self.suspended.set(true);
+        let cx = self.context();
+        cx.suspend();
         None
     }
 

+ 38 - 20
packages/core/src/virtual_dom.rs

@@ -9,6 +9,7 @@ use crate::{
     mutations::Mutation,
     nodes::RenderReturn,
     nodes::{Template, TemplateId},
+    runtime::{Runtime, RuntimeGuard},
     scopes::{ScopeId, ScopeState},
     AttributeValue, Element, Event, Scope,
 };
@@ -174,24 +175,24 @@ use std::{any::Any, cell::Cell, collections::BTreeSet, future::Future, rc::Rc};
 /// }
 /// ```
 pub struct VirtualDom {
-    // Maps a template path to a map of byteindexes to templates
-    pub(crate) templates: FxHashMap<TemplateId, FxHashMap<usize, Template<'static>>>,
     pub(crate) scopes: Slab<Box<ScopeState>>,
+
     pub(crate) dirty_scopes: BTreeSet<DirtyScope>,
-    pub(crate) scheduler: Rc<Scheduler>,
+
+    // Maps a template path to a map of byteindexes to templates
+    pub(crate) templates: FxHashMap<TemplateId, FxHashMap<usize, Template<'static>>>,
 
     // Every element is actually a dual reference - one to the template and the other to the dynamic node in that template
     pub(crate) elements: Slab<ElementRef>,
 
-    // While diffing we need some sort of way of breaking off a stream of suspended mutations.
-    pub(crate) scope_stack: Vec<ScopeId>,
+    pub(crate) mutations: Mutations<'static>,
+
+    pub(crate) runtime: Rc<Runtime>,
 
     // Currently suspended scopes
     pub(crate) suspended_scopes: FxHashSet<ScopeId>,
 
     pub(crate) rx: futures_channel::mpsc::UnboundedReceiver<SchedulerMsg>,
-
-    pub(crate) mutations: Mutations<'static>,
 }
 
 impl VirtualDom {
@@ -251,16 +252,16 @@ impl VirtualDom {
     /// ```
     pub fn new_with_props<P: 'static>(root: fn(Scope<P>) -> Element, root_props: P) -> Self {
         let (tx, rx) = futures_channel::mpsc::unbounded();
+        let scheduler = Scheduler::new(tx);
         let mut dom = Self {
             rx,
-            scheduler: Scheduler::new(tx),
-            templates: Default::default(),
+            runtime: Runtime::new(scheduler),
             scopes: Default::default(),
+            dirty_scopes: Default::default(),
+            templates: Default::default(),
             elements: Default::default(),
-            scope_stack: Vec::new(),
-            dirty_scopes: BTreeSet::new(),
-            suspended_scopes: FxHashSet::default(),
             mutations: Mutations::default(),
+            suspended_scopes: Default::default(),
         };
 
         let root = dom.new_scope(
@@ -281,7 +282,7 @@ impl VirtualDom {
     ///
     /// This is useful for inserting or removing contexts from a scope, or rendering out its root node
     pub fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> {
-        self.scopes.get(id.0).map(|f| f.as_ref())
+        self.scopes.get(id.0).map(|s| &**s)
     }
 
     /// Get the single scope at the top of the VirtualDom tree that will always be around
@@ -301,10 +302,10 @@ impl VirtualDom {
 
     /// Manually mark a scope as requiring a re-render
     ///
-    /// Whenever the VirtualDom "works", it will re-render this scope
+    /// Whenever the Runtime "works", it will re-render this scope
     pub fn mark_dirty(&mut self, id: ScopeId) {
         if let Some(scope) = self.get_scope(id) {
-            let height = scope.height;
+            let height = scope.height();
             self.dirty_scopes.insert(DirtyScope { height, id });
         }
     }
@@ -325,6 +326,8 @@ impl VirtualDom {
         element: ElementId,
         bubbles: bool,
     ) {
+        let _runtime = RuntimeGuard::new(self.runtime.clone());
+
         /*
         ------------------------
         The algorithm works by walking through the list of dynamic attributes, checking their paths, and breaking when
@@ -387,9 +390,14 @@ impl VirtualDom {
                     // We check the bubble state between each call to see if the event has been stopped from bubbling
                     for listener in listeners.drain(..).rev() {
                         if let AttributeValue::Listener(listener) = listener {
+                            let origin = el_ref.scope;
+                            self.runtime.scope_stack.borrow_mut().push(origin);
+                            self.runtime.rendering.set(false);
                             if let Some(cb) = listener.borrow_mut().as_deref_mut() {
                                 cb(uievent.clone());
                             }
+                            self.runtime.scope_stack.borrow_mut().pop();
+                            self.runtime.rendering.set(true);
 
                             if !uievent.propagates.get() {
                                 return;
@@ -418,9 +426,14 @@ impl VirtualDom {
                         // Only call the listener if this is the exact target element.
                         if attr.name.trim_start_matches("on") == name && target_path == this_path {
                             if let AttributeValue::Listener(listener) = &attr.value {
+                                let origin = el_ref.scope;
+                                self.runtime.scope_stack.borrow_mut().push(origin);
+                                self.runtime.rendering.set(false);
                                 if let Some(cb) = listener.borrow_mut().as_deref_mut() {
                                     cb(uievent.clone());
                                 }
+                                self.runtime.scope_stack.borrow_mut().pop();
+                                self.runtime.rendering.set(true);
 
                                 break;
                             }
@@ -501,10 +514,11 @@ impl VirtualDom {
                 if sync.template.get().name.rsplit_once(':').unwrap().0
                     == template.name.rsplit_once(':').unwrap().0
                 {
-                    let height = scope.height;
+                    let context = scope.context();
+                    let height = context.height;
                     self.dirty_scopes.insert(DirtyScope {
                         height,
-                        id: scope.id,
+                        id: context.id,
                     });
                 }
             }
@@ -532,6 +546,7 @@ impl VirtualDom {
     /// apply_edits(edits);
     /// ```
     pub fn rebuild(&mut self) -> Mutations {
+        let _runtime = RuntimeGuard::new(self.runtime.clone());
         match unsafe { self.run_scope(ScopeId(0)).extend_lifetime_ref() } {
             // Rebuilding implies we append the created elements to the root
             RenderReturn::Ready(node) => {
@@ -610,9 +625,12 @@ impl VirtualDom {
                     continue;
                 }
 
-                // Run the scope and get the mutations
-                self.run_scope(dirty.id);
-                self.diff_scope(dirty.id);
+                {
+                    let _runtime = RuntimeGuard::new(self.runtime.clone());
+                    // Run the scope and get the mutations
+                    self.run_scope(dirty.id);
+                    self.diff_scope(dirty.id);
+                }
             }
 
             // If there's more work, then just continue, plenty of work to do

+ 7 - 2
packages/desktop/src/protocol.rs

@@ -158,8 +158,13 @@ fn get_mime_from_path(trimmed: &Path) -> Result<&'static str> {
     }
 
     let res = match infer::get_from_path(trimmed)?.map(|f| f.mime_type()) {
-        Some(t) if t == "text/plain" => get_mime_by_ext(trimmed),
-        Some(f) => f,
+        Some(f) => {
+            if f == "text/plain" {
+                get_mime_by_ext(trimmed)
+            } else {
+                f
+            }
+        }
         None => get_mime_by_ext(trimmed),
     };
 

+ 7 - 1
packages/dioxus-tui/examples/hover.rs

@@ -1,6 +1,7 @@
 use dioxus::{events::MouseData, prelude::*};
 use dioxus_core::Event;
 use std::convert::TryInto;
+use std::fmt::Write;
 use std::rc::Rc;
 
 fn main() {
@@ -9,7 +10,12 @@ fn main() {
 
 fn app(cx: Scope) -> Element {
     fn to_str(c: &[i32; 3]) -> String {
-        "#".to_string() + &c.iter().map(|c| format!("{c:02X?}")).collect::<String>()
+        let mut result = String::new();
+        result += "#";
+        for c in c.iter() {
+            write!(result, "{c:02X?}").unwrap();
+        }
+        result
     }
 
     fn get_brightness(m: &Rc<MouseData>) -> i32 {

+ 17 - 0
packages/generational-box/Cargo.toml

@@ -0,0 +1,17 @@
+[package]
+name = "generational-box"
+authors = ["Evan Almloff"]
+version = "0.0.0"
+edition = "2018"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+bumpalo = { version = "3.6" }
+
+[dev-dependencies]
+rand = "0.8.5"
+
+[features]
+default = ["check_generation"]
+check_generation = []

+ 34 - 0
packages/generational-box/README.md

@@ -0,0 +1,34 @@
+# Generational Box
+
+Generational Box is a runtime for Rust that allows any static type to implement `Copy`. It can be combined with a global runtime to create an ergonomic state solution like `dioxus-signals`. This crate contains no `unsafe` code.
+
+Three main types manage state in Generational Box:
+
+- Store: Handles recycling generational boxes that have been dropped. Your application should have one store or one store per thread.
+- Owner: Handles dropping generational boxes. The owner acts like a runtime lifetime guard. Any states that you create with an owner will be dropped when that owner is dropped.
+- GenerationalBox: The core Copy state type. The generational box will be dropped when the owner is dropped.
+
+Example:
+
+```rust
+// Create a store for this thread
+let store = Store::default();
+
+{
+    // Create an owner for some state for a scope
+    let owner = store.owner();
+
+    // Create some non-copy data, move it into a owner, and work with copy data
+    let data: String = "hello world".to_string();
+    let key = owner.insert(data);
+    
+    // The generational box can be read from and written to like a RefCell
+    let value = key.read();
+    assert_eq!(*value, "hello world");
+}
+// Reading value at this point will cause a panic
+```
+
+## How it works
+
+Internally 

+ 359 - 0
packages/generational-box/src/lib.rs

@@ -0,0 +1,359 @@
+#![doc = include_str!("../README.md")]
+#![warn(missing_docs)]
+
+use std::{
+    cell::{Cell, Ref, RefCell, RefMut},
+    fmt::Debug,
+    marker::PhantomData,
+    rc::Rc,
+};
+
+use bumpalo::Bump;
+
+/// # Example
+///
+/// ```compile_fail
+/// let data = String::from("hello world");
+/// let store = Store::default();
+/// let owner = store.owner();
+/// let key = owner.insert(&data);
+/// drop(data);
+/// assert_eq!(*key.read(), "hello world");
+/// ```
+#[allow(unused)]
+fn compile_fail() {}
+
+#[test]
+fn reused() {
+    let store = Store::default();
+    let first_ptr;
+    {
+        let owner = store.owner();
+        first_ptr = owner.insert(1).raw.data.as_ptr();
+        drop(owner);
+    }
+    {
+        let owner = store.owner();
+        let second_ptr = owner.insert(1234).raw.data.as_ptr();
+        assert_eq!(first_ptr, second_ptr);
+        drop(owner);
+    }
+}
+
+#[test]
+fn leaking_is_ok() {
+    let data = String::from("hello world");
+    let store = Store::default();
+    let key;
+    {
+        // create an owner
+        let owner = store.owner();
+        // insert data into the store
+        key = owner.insert(data);
+        // don't drop the owner
+        std::mem::forget(owner);
+    }
+    assert_eq!(key.try_read().as_deref(), Some(&"hello world".to_string()));
+}
+
+#[test]
+fn drops() {
+    let data = String::from("hello world");
+    let store = Store::default();
+    let key;
+    {
+        // create an owner
+        let owner = store.owner();
+        // insert data into the store
+        key = owner.insert(data);
+        // drop the owner
+    }
+    assert!(key.try_read().is_none());
+}
+
+#[test]
+fn works() {
+    let store = Store::default();
+    let owner = store.owner();
+    let key = owner.insert(1);
+
+    assert_eq!(*key.read(), 1);
+}
+
+#[test]
+fn insert_while_reading() {
+    let store = Store::default();
+    let owner = store.owner();
+    let key;
+    {
+        let data: String = "hello world".to_string();
+        key = owner.insert(data);
+    }
+    let value = key.read();
+    owner.insert(&1);
+    assert_eq!(*value, "hello world");
+}
+
+#[test]
+#[should_panic]
+fn panics() {
+    let store = Store::default();
+    let owner = store.owner();
+    let key = owner.insert(1);
+    drop(owner);
+
+    assert_eq!(*key.read(), 1);
+}
+
+#[test]
+fn fuzz() {
+    fn maybe_owner_scope(
+        store: &Store,
+        valid_keys: &mut Vec<GenerationalBox<String>>,
+        invalid_keys: &mut Vec<GenerationalBox<String>>,
+        path: &mut Vec<u8>,
+    ) {
+        let branch_cutoff = 5;
+        let children = if path.len() < branch_cutoff {
+            rand::random::<u8>() % 4
+        } else {
+            rand::random::<u8>() % 2
+        };
+
+        for i in 0..children {
+            let owner = store.owner();
+            let key = owner.insert(format!("hello world {path:?}"));
+            valid_keys.push(key);
+            path.push(i);
+            // read all keys
+            println!("{:?}", path);
+            for key in valid_keys.iter() {
+                let value = key.read();
+                println!("{:?}", value);
+                assert!(value.starts_with("hello world"));
+            }
+            #[cfg(any(debug_assertions, feature = "check_generation"))]
+            for key in invalid_keys.iter() {
+                assert!(!key.validate());
+            }
+            maybe_owner_scope(store, valid_keys, invalid_keys, path);
+            invalid_keys.push(valid_keys.pop().unwrap());
+            path.pop();
+        }
+    }
+
+    for _ in 0..10 {
+        let store = Store::default();
+        maybe_owner_scope(&store, &mut Vec::new(), &mut Vec::new(), &mut Vec::new());
+    }
+}
+
+/// The core Copy state type. The generational box will be dropped when the [Owner] is dropped.
+pub struct GenerationalBox<T> {
+    raw: MemoryLocation,
+    #[cfg(any(debug_assertions, feature = "check_generation"))]
+    generation: u32,
+    _marker: PhantomData<T>,
+}
+
+impl<T: 'static> Debug for GenerationalBox<T> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        #[cfg(any(debug_assertions, feature = "check_generation"))]
+        f.write_fmt(format_args!(
+            "{:?}@{:?}",
+            self.raw.data.as_ptr(),
+            self.generation
+        ))?;
+        #[cfg(not(any(debug_assertions, feature = "check_generation")))]
+        f.write_fmt(format_args!("{:?}", self.raw.data.as_ptr()))?;
+        Ok(())
+    }
+}
+
+impl<T: 'static> GenerationalBox<T> {
+    #[inline(always)]
+    fn validate(&self) -> bool {
+        #[cfg(any(debug_assertions, feature = "check_generation"))]
+        {
+            self.raw.generation.get() == self.generation
+        }
+        #[cfg(not(any(debug_assertions, feature = "check_generation")))]
+        {
+            true
+        }
+    }
+
+    /// Try to read the value. Returns None if the value is no longer valid.
+    pub fn try_read(&self) -> Option<Ref<'_, T>> {
+        self.validate()
+            .then(|| {
+                Ref::filter_map(self.raw.data.borrow(), |any| {
+                    any.as_ref()?.downcast_ref::<T>()
+                })
+                .ok()
+            })
+            .flatten()
+    }
+
+    /// Read the value. Panics if the value is no longer valid.
+    pub fn read(&self) -> Ref<'_, T> {
+        self.try_read().unwrap()
+    }
+
+    /// Try to write the value. Returns None if the value is no longer valid.
+    pub fn try_write(&self) -> Option<RefMut<'_, T>> {
+        self.validate()
+            .then(|| {
+                RefMut::filter_map(self.raw.data.borrow_mut(), |any| {
+                    any.as_mut()?.downcast_mut::<T>()
+                })
+                .ok()
+            })
+            .flatten()
+    }
+
+    /// Write the value. Panics if the value is no longer valid.
+    pub fn write(&self) -> RefMut<'_, T> {
+        self.try_write().unwrap()
+    }
+
+    /// Set the value. Panics if the value is no longer valid.
+    pub fn set(&self, value: T) {
+        self.validate().then(|| {
+            *self.raw.data.borrow_mut() = Some(Box::new(value));
+        });
+    }
+
+    /// Returns true if the pointer is equal to the other pointer.
+    pub fn ptr_eq(&self, other: &Self) -> bool {
+        #[cfg(any(debug_assertions, feature = "check_generation"))]
+        {
+            self.raw.data.as_ptr() == other.raw.data.as_ptr() && self.generation == other.generation
+        }
+        #[cfg(not(any(debug_assertions, feature = "check_generation")))]
+        {
+            self.raw.data.as_ptr() == other.raw.data.as_ptr()
+        }
+    }
+}
+
+impl<T> Copy for GenerationalBox<T> {}
+
+impl<T> Clone for GenerationalBox<T> {
+    fn clone(&self) -> Self {
+        *self
+    }
+}
+
+#[derive(Clone, Copy)]
+struct MemoryLocation {
+    data: &'static RefCell<Option<Box<dyn std::any::Any>>>,
+    #[cfg(any(debug_assertions, feature = "check_generation"))]
+    generation: &'static Cell<u32>,
+}
+
+impl MemoryLocation {
+    #[allow(unused)]
+    fn drop(&self) {
+        let old = self.data.borrow_mut().take();
+        #[cfg(any(debug_assertions, feature = "check_generation"))]
+        if old.is_some() {
+            drop(old);
+            let new_generation = self.generation.get() + 1;
+            self.generation.set(new_generation);
+        }
+    }
+
+    fn replace<T: 'static>(&mut self, value: T) -> GenerationalBox<T> {
+        let mut inner_mut = self.data.borrow_mut();
+
+        let raw = Box::new(value);
+        let old = inner_mut.replace(raw);
+        assert!(old.is_none());
+        GenerationalBox {
+            raw: *self,
+            #[cfg(any(debug_assertions, feature = "check_generation"))]
+            generation: self.generation.get(),
+            _marker: PhantomData,
+        }
+    }
+}
+
+/// Handles recycling generational boxes that have been dropped. Your application should have one store or one store per thread.
+#[derive(Clone)]
+pub struct Store {
+    bump: &'static Bump,
+    recycled: Rc<RefCell<Vec<MemoryLocation>>>,
+}
+
+impl Default for Store {
+    fn default() -> Self {
+        Self {
+            bump: Box::leak(Box::new(Bump::new())),
+            recycled: Default::default(),
+        }
+    }
+}
+
+impl Store {
+    fn recycle(&self, location: MemoryLocation) {
+        location.drop();
+        self.recycled.borrow_mut().push(location);
+    }
+
+    fn claim(&self) -> MemoryLocation {
+        if let Some(location) = self.recycled.borrow_mut().pop() {
+            location
+        } else {
+            let data: &'static RefCell<_> = self.bump.alloc(RefCell::new(None));
+            MemoryLocation {
+                data,
+                #[cfg(any(debug_assertions, feature = "check_generation"))]
+                generation: self.bump.alloc(Cell::new(0)),
+            }
+        }
+    }
+
+    /// Create a new owner. The owner will be responsible for dropping all of the generational boxes that it creates.
+    pub fn owner(&self) -> Owner {
+        Owner {
+            store: self.clone(),
+            owned: Default::default(),
+        }
+    }
+}
+
+/// Owner: Handles dropping generational boxes. The owner acts like a runtime lifetime guard. Any states that you create with an owner will be dropped when that owner is dropped.
+pub struct Owner {
+    store: Store,
+    owned: Rc<RefCell<Vec<MemoryLocation>>>,
+}
+
+impl Owner {
+    /// Insert a value into the store. The value will be dropped when the owner is dropped.
+    pub fn insert<T: 'static>(&self, value: T) -> GenerationalBox<T> {
+        let mut location = self.store.claim();
+        let key = location.replace(value);
+        self.owned.borrow_mut().push(location);
+        key
+    }
+
+    /// Creates an invalid handle. This is useful for creating a handle that will be filled in later. If you use this before the value is filled in, you will get may get a panic or an out of date value.
+    pub fn invalid<T: 'static>(&self) -> GenerationalBox<T> {
+        let location = self.store.claim();
+        GenerationalBox {
+            raw: location,
+            #[cfg(any(debug_assertions, feature = "check_generation"))]
+            generation: location.generation.get(),
+            _marker: PhantomData,
+        }
+    }
+}
+
+impl Drop for Owner {
+    fn drop(&mut self) {
+        for location in self.owned.borrow().iter() {
+            self.store.recycle(*location)
+        }
+    }
+}

+ 2 - 0
packages/hooks/src/computed.rs

@@ -1,3 +1,5 @@
+//! Tracked and computed state in Dioxus
+
 use dioxus_core::{ScopeId, ScopeState};
 use slab::Slab;
 use std::{

+ 1 - 2
packages/hooks/src/lib.rs

@@ -52,8 +52,7 @@ macro_rules! to_owned {
     };
 }
 
-mod computed;
-pub use computed::*;
+pub mod computed;
 
 mod use_on_unmount;
 pub use use_on_unmount::*;

+ 1 - 1
packages/html/src/eval.rs

@@ -42,7 +42,7 @@ pub fn use_eval(cx: &ScopeState) -> &EvalCreator {
         Rc::new(move |script: &str| {
             eval_provider
                 .new_evaluator(script.to_string())
-                .map(|evaluator| UseEval::new(evaluator))
+                .map(UseEval::new)
         }) as Rc<dyn Fn(&str) -> Result<UseEval, EvalError>>
     })
 }

+ 0 - 1
packages/native-core/src/real_dom.rs

@@ -446,7 +446,6 @@ impl<V: FromAnyValue + Send + Sync> RealDom<V> {
             drop(tree);
             children.reverse();
             if let Some(node) = self.get_mut(id) {
-                let node = node;
                 f(node);
                 stack.extend(children.iter());
             }

+ 2 - 8
packages/rsx/src/lib.rs

@@ -288,10 +288,7 @@ impl DynamicMapping {
         let idx = self.last_attribute_idx;
         self.last_attribute_idx += 1;
 
-        self.attribute_to_idx
-            .entry(attr)
-            .or_insert_with(Vec::new)
-            .push(idx);
+        self.attribute_to_idx.entry(attr).or_default().push(idx);
 
         idx
     }
@@ -300,10 +297,7 @@ impl DynamicMapping {
         let idx = self.last_element_idx;
         self.last_element_idx += 1;
 
-        self.node_to_idx
-            .entry(node)
-            .or_insert_with(Vec::new)
-            .push(idx);
+        self.node_to_idx.entry(node).or_default().push(idx);
 
         idx
     }

+ 13 - 1
packages/signals/Cargo.toml

@@ -8,4 +8,16 @@ edition = "2018"
 
 [dependencies]
 dioxus-core = { workspace = true }
-slab = { workspace = true }
+generational-box = { workspace = true }
+log.workspace = true
+simple_logger = "4.2.0"
+serde = { version = "1", features = ["derive"], optional = true }
+
+[dev-dependencies]
+dioxus = { workspace = true }
+dioxus-desktop = { workspace = true }
+tokio = { version = "1", features = ["full"] }
+
+[features]
+default = []
+serialize = ["serde"]

+ 122 - 0
packages/signals/README.md

@@ -0,0 +1,122 @@
+# Dioxus Signals
+
+Dioxus Signals is an ergonomic Copy runtime for data with local subscriptions.
+
+## Copy Data
+
+All signals implement Copy, even if the inner value does not implement copy. This makes it easy to move any data into futures or children.
+
+```rust
+use dioxus::prelude::*;
+use dioxus_signals::*;
+
+fn app(cx: Scope) -> Element {
+    let signal = use_signal(cx, || "hello world".to_string());
+
+    spawn(async move {
+        // signal is Copy even though String is not copy
+        print!("{signal}");
+    });
+
+    render! {
+        "{signal}"
+    }
+}
+```
+
+## Local Subscriptions
+
+Signals will only subscribe to components when you read from the signal in that component. It will never subscribe to a component when reading data in a future or event handler.
+
+```rust
+use dioxus::prelude::*;
+use dioxus_signals::*;
+
+fn app(cx: Scope) -> Element {
+    // Because signal is never read in this component, this component will not rerun when the signal changes
+    let signal = use_signal(cx, || 0);
+
+    render! {
+        button {
+            onclick: move |_| {
+                *signal.write() += 1;
+            },
+            "Increase"
+        }
+        for id in 0..10 {
+            Child {
+                signal: signal,
+            }
+        }
+    }
+}
+
+#[derive(Props, Clone, PartialEq)]
+struct ChildProps {
+    signal: Signal<usize>,
+}
+
+fn Child(cx: Scope<ChildProps>) -> Element {
+    // This component does read from the signal, so when the signal changes it will rerun
+    render! {
+        "{cx.props.signal}"
+    }
+}
+```
+
+Because subscriptions happen when you read from (not create) the data, you can provide signals through the normal context API:
+
+```rust
+use dioxus::prelude::*;
+use dioxus_signals::*;
+
+fn app(cx: Scope) -> Element {
+    // Because signal is never read in this component, this component will not rerun when the signal changes
+    use_context_provider(cx, || Signal::new(0));
+
+    render! {
+        Child {}
+    }
+}
+
+fn Child(cx: Scope) -> Element {
+    let signal: Signal<i32> = *use_context(cx).unwrap();
+    // This component does read from the signal, so when the signal changes it will rerun
+    render! {
+        "{signal}"
+    }
+}
+```
+
+## Computed Data
+
+In addition to local subscriptions in components, `dioxus-signals` provides a way to derive data with local subscriptions.
+
+The use_selector hook will only rerun when any signals inside of the hook change:
+
+```rust
+use dioxus::prelude::*;
+use dioxus_signals::*;
+
+fn app(cx: Scope) -> Element {
+    let signal = use_signal(cx, || 0);
+    let doubled = use_selector(cx, || signal * 2);
+
+    render! {
+        button {
+            onclick: move |_| *signal.write() += 1,
+            "Increase"
+        }
+        Child {
+            signal: signal
+        }
+    }
+}
+
+#[inline_props]
+fn Child(cx: Scope, signal: ReadOnlySignal<usize>) -> Element {
+    render! {
+        "{signal}"
+    }
+}
+```

+ 25 - 0
packages/signals/examples/context.rs

@@ -0,0 +1,25 @@
+#![allow(non_snake_case)]
+
+use dioxus::prelude::*;
+use dioxus_signals::Signal;
+
+fn main() {
+    dioxus_desktop::launch(app);
+}
+
+fn app(cx: Scope) -> Element {
+    // Because signal is never read in this component, this component will not rerun when the signal changes
+    use_context_provider(cx, || Signal::new(0));
+
+    render! {
+        Child {}
+    }
+}
+
+fn Child(cx: Scope) -> Element {
+    let signal: Signal<i32> = *use_context(cx).unwrap();
+    // This component does read from the signal, so when the signal changes it will rerun
+    render! {
+        "{signal}"
+    }
+}

+ 38 - 0
packages/signals/examples/dependancies.rs

@@ -0,0 +1,38 @@
+#![allow(non_snake_case)]
+
+use dioxus::prelude::*;
+use dioxus_signals::*;
+
+fn main() {
+    dioxus_desktop::launch(app);
+}
+
+fn app(cx: Scope) -> Element {
+    let signal = use_signal(cx, || 0);
+
+    use_future!(cx, || async move {
+        loop {
+            tokio::time::sleep(std::time::Duration::from_secs(1)).await;
+            *signal.write() += 1;
+        }
+    });
+
+    let local_state = use_state(cx, || 0);
+    let computed =
+        use_selector_with_dependencies(cx, (local_state.get(),), move |(local_state,)| {
+            local_state * 2 + signal.value()
+        });
+    println!("Running app");
+
+    render! {
+        button {
+            onclick: move |_| {
+                local_state.set(local_state.get() + 1);
+            },
+            "Add one"
+        }
+        div {
+            "{computed}"
+        }
+    }
+}

+ 30 - 0
packages/signals/examples/selector.rs

@@ -0,0 +1,30 @@
+#![allow(non_snake_case)]
+
+use dioxus::prelude::*;
+use dioxus_signals::*;
+
+fn main() {
+    dioxus_desktop::launch(app);
+}
+
+fn app(cx: Scope) -> Element {
+    let signal = use_signal(cx, || 0);
+    let doubled = use_selector(cx, move || signal * 2);
+
+    render! {
+        button {
+            onclick: move |_| *signal.write() += 1,
+            "Increase"
+        }
+        Child {
+            signal: doubled
+        }
+    }
+}
+
+#[inline_props]
+fn Child(cx: Scope, signal: ReadOnlySignal<usize>) -> Element {
+    render! {
+        "{signal}"
+    }
+}

+ 150 - 0
packages/signals/examples/split_subscriptions.rs

@@ -0,0 +1,150 @@
+#![allow(non_snake_case)]
+
+use dioxus::prelude::*;
+use dioxus_signals::Signal;
+
+fn main() {
+    dioxus_desktop::launch(app);
+}
+
+#[derive(Clone, Copy, Default)]
+struct ApplicationData {
+    first_data: Signal<i32>,
+    second_data: Signal<i32>,
+    many_signals: Signal<Vec<Signal<i32>>>,
+}
+
+fn use_app_data(cx: Scope) -> ApplicationData {
+    *use_context(cx).unwrap()
+}
+
+fn app(cx: Scope) -> Element {
+    use_context_provider(cx, ApplicationData::default);
+
+    render! {
+        div {
+            ReadsFirst {}
+        }
+        div {
+            ReadsSecond {}
+        }
+        div {
+            ReadsManySignals {}
+        }
+    }
+}
+
+fn ReadsFirst(cx: Scope) -> Element {
+    println!("running first");
+    let data = use_app_data(cx);
+
+    render! {
+        button {
+            onclick: move |_| {
+                *data.first_data.write() += 1;
+            },
+            "Increase"
+        }
+        button {
+            onclick: move |_| {
+                *data.first_data.write() -= 1;
+            },
+            "Decrease"
+        }
+        button {
+            onclick: move |_| {
+                *data.first_data.write() = 0;
+            },
+            "Reset"
+        }
+        "{data.first_data}"
+    }
+}
+
+fn ReadsSecond(cx: Scope) -> Element {
+    println!("running second");
+    let data = use_app_data(cx);
+
+    render! {
+        button {
+            onclick: move |_| {
+                *data.second_data.write() += 1;
+            },
+            "Increase"
+        }
+        button {
+            onclick: move |_| {
+                *data.second_data.write() -= 1;
+            },
+            "Decrease"
+        }
+        button {
+            onclick: move |_| {
+                *data.second_data.write() = 0;
+            },
+            "Reset"
+        }
+        "{data.second_data}"
+    }
+}
+
+fn ReadsManySignals(cx: Scope) -> Element {
+    println!("running many signals");
+    let data = use_app_data(cx);
+
+    render! {
+        button {
+            onclick: move |_| {
+                data.many_signals.write().push(Signal::new(0));
+            },
+            "Create"
+        }
+        button {
+            onclick: move |_| {
+                data.many_signals.write().pop();
+            },
+            "Destroy"
+        }
+        button {
+            onclick: move |_| {
+                if let Some(first) = data.many_signals.read().get(0) {
+                    *first.write() += 1;
+                }
+            },
+            "Increase First Item"
+        }
+        for signal in data.many_signals {
+            Child {
+                count: signal,
+            }
+        }
+    }
+}
+
+#[derive(Props, PartialEq)]
+struct ChildProps {
+    count: Signal<i32>,
+}
+
+fn Child(cx: Scope<ChildProps>) -> Element {
+    println!("running child");
+    let count = cx.props.count;
+
+    render! {
+        div {
+            "Child: {count}"
+            button {
+                onclick: move |_| {
+                    *count.write() += 1;
+                },
+                "Increase"
+            }
+            button {
+                onclick: move |_| {
+                    *count.write() -= 1;
+                },
+                "Decrease"
+            }
+        }
+    }
+}

+ 67 - 0
packages/signals/src/dependency.rs

@@ -0,0 +1,67 @@
+/// A dependency is a trait that can be used to determine if a effect or selector should be re-run.
+pub trait Dependency: Sized + Clone {
+    /// The output of the dependency
+    type Out: Clone + PartialEq;
+    /// Returns the output of the dependency.
+    fn out(&self) -> Self::Out;
+    /// Returns true if the dependency has changed.
+    fn changed(&self, other: &Self::Out) -> bool {
+        self.out() != *other
+    }
+}
+
+impl Dependency for () {
+    type Out = ();
+    fn out(&self) -> Self::Out {}
+}
+
+/// A dependency is a trait that can be used to determine if a effect or selector should be re-run.
+pub trait Dep: 'static + PartialEq + Clone {}
+impl<T> Dep for T where T: 'static + PartialEq + Clone {}
+
+impl<A: Dep> Dependency for &A {
+    type Out = A;
+    fn out(&self) -> Self::Out {
+        (*self).clone()
+    }
+}
+
+macro_rules! impl_dep {
+    (
+        $($el:ident=$name:ident $other:ident,)*
+    ) => {
+        impl< $($el),* > Dependency for ($(&$el,)*)
+        where
+            $(
+                $el: Dep
+            ),*
+        {
+            type Out = ($($el,)*);
+
+            fn out(&self) -> Self::Out {
+                let ($($name,)*) = self;
+                ($((*$name).clone(),)*)
+            }
+
+            fn changed(&self, other: &Self::Out) -> bool {
+                let ($($name,)*) = self;
+                let ($($other,)*) = other;
+                $(
+                    if *$name != $other {
+                        return true;
+                    }
+                )*
+                false
+            }
+        }
+    };
+}
+
+impl_dep!(A = a1 a2,);
+impl_dep!(A = a1 a2, B = b1 b2,);
+impl_dep!(A = a1 a2, B = b1 b2, C = c1 c2,);
+impl_dep!(A = a1 a2, B = b1 b2, C = c1 c2, D = d1 d2,);
+impl_dep!(A = a1 a2, B = b1 b2, C = c1 c2, D = d1 d2, E = e1 e2,);
+impl_dep!(A = a1 a2, B = b1 b2, C = c1 c2, D = d1 d2, E = e1 e2, F = f1 f2,);
+impl_dep!(A = a1 a2, B = b1 b2, C = c1 c2, D = d1 d2, E = e1 e2, F = f1 f2, G = g1 g2,);
+impl_dep!(A = a1 a2, B = b1 b2, C = c1 c2, D = d1 d2, E = e1 e2, F = f1 f2, G = g1 g2, H = h1 h2,);

+ 96 - 0
packages/signals/src/effect.rs

@@ -0,0 +1,96 @@
+use core::{self, fmt::Debug};
+use std::cell::RefCell;
+use std::fmt::{self, Formatter};
+use std::rc::Rc;
+//
+use dioxus_core::prelude::*;
+
+use crate::use_signal;
+use crate::{dependency::Dependency, CopyValue};
+
+#[derive(Default, Clone)]
+pub(crate) struct EffectStack {
+    pub(crate) effects: Rc<RefCell<Vec<Effect>>>,
+}
+
+pub(crate) fn get_effect_stack() -> EffectStack {
+    match consume_context() {
+        Some(rt) => rt,
+        None => {
+            let store = EffectStack::default();
+            provide_root_context(store).expect("in a virtual dom")
+        }
+    }
+}
+
+/// Create a new effect. The effect will be run immediately and whenever any signal it reads changes.
+/// The signal will be owned by the current component and will be dropped when the component is dropped.
+pub fn use_effect(cx: &ScopeState, callback: impl FnMut() + 'static) {
+    cx.use_hook(|| Effect::new(callback));
+}
+
+/// Create a new effect. The effect will be run immediately and whenever any signal it reads changes.
+/// The signal will be owned by the current component and will be dropped when the component is dropped.
+pub fn use_effect_with_dependencies<D: Dependency>(
+    cx: &ScopeState,
+    dependencies: D,
+    mut callback: impl FnMut(D::Out) + 'static,
+) where
+    D::Out: 'static,
+{
+    let dependencies_signal = use_signal(cx, || dependencies.out());
+    cx.use_hook(|| {
+        Effect::new(move || {
+            let deref = &*dependencies_signal.read();
+            callback(deref.clone());
+        });
+    });
+    let changed = { dependencies.changed(&*dependencies_signal.read()) };
+    if changed {
+        dependencies_signal.set(dependencies.out());
+    }
+}
+
+/// Effects allow you to run code when a signal changes. Effects are run immediately and whenever any signal it reads changes.
+#[derive(Copy, Clone, PartialEq)]
+pub struct Effect {
+    pub(crate) callback: CopyValue<Box<dyn FnMut()>>,
+}
+
+impl Debug for Effect {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        f.write_fmt(format_args!("{:?}", self.callback.value))
+    }
+}
+
+impl Effect {
+    pub(crate) fn current() -> Option<Self> {
+        get_effect_stack().effects.borrow().last().copied()
+    }
+
+    /// Create a new effect. The effect will be run immediately and whenever any signal it reads changes.
+    ///
+    /// The signal will be owned by the current component and will be dropped when the component is dropped.
+    pub fn new(callback: impl FnMut() + 'static) -> Self {
+        let myself = Self {
+            callback: CopyValue::new(Box::new(callback)),
+        };
+
+        myself.try_run();
+
+        myself
+    }
+
+    /// Run the effect callback immediately. Returns `true` if the effect was run. Returns `false` is the effect is dead.
+    pub fn try_run(&self) {
+        if let Some(mut callback) = self.callback.try_write() {
+            {
+                get_effect_stack().effects.borrow_mut().push(*self);
+            }
+            callback();
+            {
+                get_effect_stack().effects.borrow_mut().pop();
+            }
+        }
+    }
+}

+ 294 - 0
packages/signals/src/impls.rs

@@ -0,0 +1,294 @@
+use crate::rt::CopyValue;
+use crate::signal::{ReadOnlySignal, Signal, Write};
+
+use std::cell::{Ref, RefMut};
+
+use std::{
+    fmt::{Debug, Display},
+    ops::{Add, Div, Mul, Sub},
+};
+
+macro_rules! read_impls {
+    ($ty:ident) => {
+        impl<T: Default + 'static> Default for $ty<T> {
+            fn default() -> Self {
+                Self::new(Default::default())
+            }
+        }
+
+        impl<T> std::clone::Clone for $ty<T> {
+            fn clone(&self) -> Self {
+                *self
+            }
+        }
+
+        impl<T> Copy for $ty<T> {}
+
+        impl<T: Display + 'static> Display for $ty<T> {
+            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+                self.with(|v| Display::fmt(v, f))
+            }
+        }
+
+        impl<T: Debug + 'static> Debug for $ty<T> {
+            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+                self.with(|v| Debug::fmt(v, f))
+            }
+        }
+
+        impl<T: 'static> $ty<Vec<T>> {
+            /// Read a value from the inner vector.
+            pub fn get(&self, index: usize) -> Option<Ref<'_, T>> {
+                Ref::filter_map(self.read(), |v| v.get(index)).ok()
+            }
+        }
+
+        impl<T: 'static> $ty<Option<T>> {
+            /// Unwraps the inner value and clones it.
+            pub fn unwrap(&self) -> T
+            where
+                T: Clone,
+            {
+                self.with(|v| v.clone()).unwrap()
+            }
+
+            /// Attemps to read the inner value of the Option.
+            pub fn as_ref(&self) -> Option<Ref<'_, T>> {
+                Ref::filter_map(self.read(), |v| v.as_ref()).ok()
+            }
+        }
+    };
+}
+
+macro_rules! write_impls {
+    ($ty:ident) => {
+        impl<T: Add<Output = T> + Copy + 'static> std::ops::Add<T> for $ty<T> {
+            type Output = T;
+
+            fn add(self, rhs: T) -> Self::Output {
+                self.with(|v| *v + rhs)
+            }
+        }
+
+        impl<T: Add<Output = T> + Copy + 'static> std::ops::AddAssign<T> for $ty<T> {
+            fn add_assign(&mut self, rhs: T) {
+                self.with_mut(|v| *v = *v + rhs)
+            }
+        }
+
+        impl<T: Sub<Output = T> + Copy + 'static> std::ops::SubAssign<T> for $ty<T> {
+            fn sub_assign(&mut self, rhs: T) {
+                self.with_mut(|v| *v = *v - rhs)
+            }
+        }
+
+        impl<T: Sub<Output = T> + Copy + 'static> std::ops::Sub<T> for $ty<T> {
+            type Output = T;
+
+            fn sub(self, rhs: T) -> Self::Output {
+                self.with(|v| *v - rhs)
+            }
+        }
+
+        impl<T: Mul<Output = T> + Copy + 'static> std::ops::MulAssign<T> for $ty<T> {
+            fn mul_assign(&mut self, rhs: T) {
+                self.with_mut(|v| *v = *v * rhs)
+            }
+        }
+
+        impl<T: Mul<Output = T> + Copy + 'static> std::ops::Mul<T> for $ty<T> {
+            type Output = T;
+
+            fn mul(self, rhs: T) -> Self::Output {
+                self.with(|v| *v * rhs)
+            }
+        }
+
+        impl<T: Div<Output = T> + Copy + 'static> std::ops::DivAssign<T> for $ty<T> {
+            fn div_assign(&mut self, rhs: T) {
+                self.with_mut(|v| *v = *v / rhs)
+            }
+        }
+
+        impl<T: Div<Output = T> + Copy + 'static> std::ops::Div<T> for $ty<T> {
+            type Output = T;
+
+            fn div(self, rhs: T) -> Self::Output {
+                self.with(|v| *v / rhs)
+            }
+        }
+
+        impl<T: 'static> $ty<Vec<T>> {
+            /// Pushes a new value to the end of the vector.
+            pub fn push(&self, value: T) {
+                self.with_mut(|v| v.push(value))
+            }
+
+            /// Pops the last value from the vector.
+            pub fn pop(&self) -> Option<T> {
+                self.with_mut(|v| v.pop())
+            }
+
+            /// Inserts a new value at the given index.
+            pub fn insert(&self, index: usize, value: T) {
+                self.with_mut(|v| v.insert(index, value))
+            }
+
+            /// Removes the value at the given index.
+            pub fn remove(&self, index: usize) -> T {
+                self.with_mut(|v| v.remove(index))
+            }
+
+            /// Clears the vector, removing all values.
+            pub fn clear(&self) {
+                self.with_mut(|v| v.clear())
+            }
+
+            /// Extends the vector with the given iterator.
+            pub fn extend(&self, iter: impl IntoIterator<Item = T>) {
+                self.with_mut(|v| v.extend(iter))
+            }
+
+            /// Truncates the vector to the given length.
+            pub fn truncate(&self, len: usize) {
+                self.with_mut(|v| v.truncate(len))
+            }
+
+            /// Swaps two values in the vector.
+            pub fn swap_remove(&self, index: usize) -> T {
+                self.with_mut(|v| v.swap_remove(index))
+            }
+
+            /// Retains only the values that match the given predicate.
+            pub fn retain(&self, f: impl FnMut(&T) -> bool) {
+                self.with_mut(|v| v.retain(f))
+            }
+
+            /// Splits the vector into two at the given index.
+            pub fn split_off(&self, at: usize) -> Vec<T> {
+                self.with_mut(|v| v.split_off(at))
+            }
+        }
+
+        impl<T: 'static> $ty<Option<T>> {
+            /// Takes the value out of the Option.
+            pub fn take(&self) -> Option<T> {
+                self.with_mut(|v| v.take())
+            }
+
+            /// Replace the value in the Option.
+            pub fn replace(&self, value: T) -> Option<T> {
+                self.with_mut(|v| v.replace(value))
+            }
+
+            /// Gets the value out of the Option, or inserts the given value if the Option is empty.
+            pub fn get_or_insert(&self, default: T) -> Ref<'_, T> {
+                self.get_or_insert_with(|| default)
+            }
+
+            /// Gets the value out of the Option, or inserts the value returned by the given function if the Option is empty.
+            pub fn get_or_insert_with(&self, default: impl FnOnce() -> T) -> Ref<'_, T> {
+                let borrow = self.read();
+                if borrow.is_none() {
+                    drop(borrow);
+                    self.with_mut(|v| *v = Some(default()));
+                    Ref::map(self.read(), |v| v.as_ref().unwrap())
+                } else {
+                    Ref::map(borrow, |v| v.as_ref().unwrap())
+                }
+            }
+        }
+    };
+}
+
+read_impls!(CopyValue);
+write_impls!(CopyValue);
+read_impls!(Signal);
+write_impls!(Signal);
+read_impls!(ReadOnlySignal);
+
+/// An iterator over the values of a `CopyValue<Vec<T>>`.
+pub struct CopyValueIterator<T: 'static> {
+    index: usize,
+    value: CopyValue<Vec<T>>,
+}
+
+impl<T: Clone> Iterator for CopyValueIterator<T> {
+    type Item = T;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        let index = self.index;
+        self.index += 1;
+        self.value.get(index).map(|v| v.clone())
+    }
+}
+
+impl<T: Clone + 'static> IntoIterator for CopyValue<Vec<T>> {
+    type IntoIter = CopyValueIterator<T>;
+
+    type Item = T;
+
+    fn into_iter(self) -> Self::IntoIter {
+        CopyValueIterator {
+            index: 0,
+            value: self,
+        }
+    }
+}
+
+impl<T: 'static> CopyValue<Vec<T>> {
+    /// Write to an element in the inner vector.
+    pub fn get_mut(&self, index: usize) -> Option<RefMut<'_, T>> {
+        RefMut::filter_map(self.write(), |v| v.get_mut(index)).ok()
+    }
+}
+
+impl<T: 'static> CopyValue<Option<T>> {
+    /// Deref the inner value mutably.
+    pub fn as_mut(&self) -> Option<RefMut<'_, T>> {
+        RefMut::filter_map(self.write(), |v| v.as_mut()).ok()
+    }
+}
+
+/// An iterator over items in a `Signal<Vec<T>>`.
+pub struct SignalIterator<T: 'static> {
+    index: usize,
+    value: Signal<Vec<T>>,
+}
+
+impl<T: Clone> Iterator for SignalIterator<T> {
+    type Item = T;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        let index = self.index;
+        self.index += 1;
+        self.value.get(index).map(|v| v.clone())
+    }
+}
+
+impl<T: Clone + 'static> IntoIterator for Signal<Vec<T>> {
+    type IntoIter = SignalIterator<T>;
+
+    type Item = T;
+
+    fn into_iter(self) -> Self::IntoIter {
+        SignalIterator {
+            index: 0,
+            value: self,
+        }
+    }
+}
+
+impl<T: 'static> Signal<Vec<T>> {
+    /// Returns a reference to an element or `None` if out of bounds.
+    pub fn get_mut(&self, index: usize) -> Option<Write<'_, T, Vec<T>>> {
+        Write::filter_map(self.write(), |v| v.get_mut(index))
+    }
+}
+
+impl<T: 'static> Signal<Option<T>> {
+    /// Returns a reference to an element or `None` if out of bounds.
+    pub fn as_mut(&self) -> Option<Write<'_, T, Option<T>>> {
+        Write::filter_map(self.write(), |v| v.as_mut())
+    }
+}

+ 11 - 128
packages/signals/src/lib.rs

@@ -1,131 +1,14 @@
-use std::{
-    cell::{Ref, RefMut},
-    fmt::Display,
-    marker::PhantomData,
-    ops::{Add, Div, Mul, Sub},
-};
+#![doc = include_str!("../README.md")]
+#![warn(missing_docs)]
 
 mod rt;
-
-use dioxus_core::ScopeState;
 pub use rt::*;
-
-pub fn use_init_signal_rt(cx: &ScopeState) {
-    cx.use_hook(|| {
-        let rt = claim_rt(cx.schedule_update_any());
-        cx.provide_context(rt);
-    });
-}
-
-pub fn use_signal<T: 'static>(cx: &ScopeState, f: impl FnOnce() -> T) -> Signal<T> {
-    cx.use_hook(|| {
-        let rt: &'static SignalRt = match cx.consume_context() {
-            Some(rt) => rt,
-            None => cx.provide_context(claim_rt(cx.schedule_update_any())),
-        };
-
-        let id = rt.init(f());
-        rt.subscribe(id, cx.scope_id());
-
-        struct SignalHook<T> {
-            signal: Signal<T>,
-        }
-
-        impl<T> Drop for SignalHook<T> {
-            fn drop(&mut self) {
-                self.signal.rt.remove(self.signal.id);
-            }
-        }
-
-        SignalHook {
-            signal: Signal {
-                id,
-                rt,
-                t: PhantomData,
-            },
-        }
-    })
-    .signal
-}
-
-pub struct Signal<T> {
-    id: usize,
-    rt: &'static SignalRt,
-    t: PhantomData<T>,
-}
-
-impl<T: 'static> Signal<T> {
-    pub fn read(&self) -> Ref<T> {
-        self.rt.read(self.id)
-    }
-
-    pub fn write(&self) -> RefMut<T> {
-        self.rt.write(self.id)
-    }
-
-    pub fn set(&mut self, value: T) {
-        self.rt.set(self.id, value);
-    }
-
-    pub fn with<O>(&self, f: impl FnOnce(&T) -> O) -> O {
-        let write = self.read();
-        f(&*write)
-    }
-
-    pub fn update<O>(&self, _f: impl FnOnce(&mut T) -> O) -> O {
-        let mut write = self.write();
-        _f(&mut *write)
-    }
-}
-
-impl<T: Clone + 'static> Signal<T> {
-    pub fn get(&self) -> T {
-        self.rt.get(self.id)
-    }
-}
-
-impl<T: Clone + 'static> std::ops::Deref for Signal<T> {
-    type Target = dyn Fn() -> T;
-
-    fn deref(&self) -> &Self::Target {
-        self.rt.getter(self.id)
-    }
-}
-
-impl<T> std::clone::Clone for Signal<T> {
-    fn clone(&self) -> Self {
-        *self
-    }
-}
-
-impl<T> Copy for Signal<T> {}
-
-impl<T: Display + 'static> Display for Signal<T> {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        self.rt.with::<T, _>(self.id, |v| T::fmt(v, f))
-    }
-}
-
-impl<T: Add<Output = T> + Copy + 'static> std::ops::AddAssign<T> for Signal<T> {
-    fn add_assign(&mut self, rhs: T) {
-        self.set(self.get() + rhs);
-    }
-}
-
-impl<T: Sub<Output = T> + Copy + 'static> std::ops::SubAssign<T> for Signal<T> {
-    fn sub_assign(&mut self, rhs: T) {
-        self.set(self.get() - rhs);
-    }
-}
-
-impl<T: Mul<Output = T> + Copy + 'static> std::ops::MulAssign<T> for Signal<T> {
-    fn mul_assign(&mut self, rhs: T) {
-        self.set(self.get() * rhs);
-    }
-}
-
-impl<T: Div<Output = T> + Copy + 'static> std::ops::DivAssign<T> for Signal<T> {
-    fn div_assign(&mut self, rhs: T) {
-        self.set(self.get() / rhs);
-    }
-}
+mod effect;
+pub use effect::*;
+mod impls;
+mod selector;
+pub use selector::*;
+pub(crate) mod signal;
+pub use signal::*;
+mod dependency;
+pub use dependency::*;

+ 120 - 82
packages/signals/src/rt.rs

@@ -1,121 +1,159 @@
-use std::{any::Any, cell::RefCell, sync::Arc};
+use std::cell::{Ref, RefMut};
 
+use std::rc::Rc;
+
+use dioxus_core::prelude::{
+    consume_context, consume_context_from_scope, current_scope_id, provide_context,
+    provide_context_to_scope, provide_root_context,
+};
 use dioxus_core::ScopeId;
-use slab::Slab;
 
-thread_local! {
-    // we cannot drop these since any future might be using them
-    static RUNTIMES: RefCell<Vec<&'static SignalRt>> = RefCell::new(Vec::new());
-}
+use generational_box::{GenerationalBox, Owner, Store};
 
-/// Provide the runtime for signals
-///
-/// This will reuse dead runtimes
-pub fn claim_rt(update_any: Arc<dyn Fn(ScopeId)>) -> &'static SignalRt {
-    RUNTIMES.with(|runtimes| {
-        if let Some(rt) = runtimes.borrow_mut().pop() {
-            return rt;
+fn current_store() -> Store {
+    match consume_context() {
+        Some(rt) => rt,
+        None => {
+            let store = Store::default();
+            provide_root_context(store).expect("in a virtual dom")
         }
+    }
+}
 
-        Box::leak(Box::new(SignalRt {
-            signals: RefCell::new(Slab::new()),
-            update_any,
-        }))
-    })
+fn current_owner() -> Rc<Owner> {
+    match consume_context() {
+        Some(rt) => rt,
+        None => {
+            let owner = Rc::new(current_store().owner());
+            provide_context(owner).expect("in a virtual dom")
+        }
+    }
 }
 
-/// Push this runtime into the global runtime list
-pub fn reclam_rt(_rt: &'static SignalRt) {
-    RUNTIMES.with(|runtimes| {
-        runtimes.borrow_mut().push(_rt);
-    });
+fn owner_in_scope(scope: ScopeId) -> Rc<Owner> {
+    match consume_context_from_scope(scope) {
+        Some(rt) => rt,
+        None => {
+            let owner = Rc::new(current_store().owner());
+            provide_context_to_scope(scope, owner).expect("in a virtual dom")
+        }
+    }
 }
 
-pub struct SignalRt {
-    pub(crate) signals: RefCell<Slab<Inner>>,
-    pub(crate) update_any: Arc<dyn Fn(ScopeId)>,
+/// CopyValue is a wrapper around a value to make the value mutable and Copy.
+///
+/// It is internally backed by [`generational_box::GenerationalBox`].
+pub struct CopyValue<T: 'static> {
+    pub(crate) value: GenerationalBox<T>,
+    origin_scope: ScopeId,
 }
 
-impl SignalRt {
-    pub fn init<T: 'static>(&'static self, val: T) -> usize {
-        self.signals.borrow_mut().insert(Inner {
-            value: Box::new(val),
-            subscribers: Vec::new(),
-            getter: None,
-        })
+#[cfg(feature = "serde")]
+impl<T: 'static> serde::Serialize for CopyValue<T>
+where
+    T: serde::Serialize,
+{
+    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        self.value.read().serialize(serializer)
     }
+}
+
+#[cfg(feature = "serde")]
+impl<'de, T: 'static> serde::Deserialize<'de> for CopyValue<T>
+where
+    T: serde::Deserialize<'de>,
+{
+    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
+        let value = T::deserialize(deserializer)?;
 
-    pub fn subscribe(&self, id: usize, subscriber: ScopeId) {
-        self.signals.borrow_mut()[id].subscribers.push(subscriber);
+        Ok(Self::new(value))
     }
+}
 
-    pub fn get<T: Clone + 'static>(&self, id: usize) -> T {
-        self.signals.borrow()[id]
-            .value
-            .downcast_ref::<T>()
-            .cloned()
-            .unwrap()
+impl<T: 'static> CopyValue<T> {
+    /// Create a new CopyValue. The value will be stored in the current component.
+    ///
+    /// Once the component this value is created in is dropped, the value will be dropped.
+    pub fn new(value: T) -> Self {
+        let owner = current_owner();
+
+        Self {
+            value: owner.insert(value),
+            origin_scope: current_scope_id().expect("in a virtual dom"),
+        }
     }
 
-    pub fn set<T: 'static>(&self, id: usize, value: T) {
-        let mut signals = self.signals.borrow_mut();
-        let inner = &mut signals[id];
-        inner.value = Box::new(value);
+    /// Create a new CopyValue. The value will be stored in the given scope. When the specified scope is dropped, the value will be dropped.
+    pub fn new_in_scope(value: T, scope: ScopeId) -> Self {
+        let owner = owner_in_scope(scope);
 
-        for subscriber in inner.subscribers.iter() {
-            (self.update_any)(*subscriber);
+        Self {
+            value: owner.insert(value),
+            origin_scope: scope,
         }
     }
 
-    pub fn remove(&self, id: usize) {
-        self.signals.borrow_mut().remove(id);
+    pub(crate) fn invalid() -> Self {
+        let owner = current_owner();
+
+        Self {
+            value: owner.invalid(),
+            origin_scope: current_scope_id().expect("in a virtual dom"),
+        }
     }
 
-    pub fn with<T: 'static, O>(&self, id: usize, f: impl FnOnce(&T) -> O) -> O {
-        let signals = self.signals.borrow();
-        let inner = &signals[id];
-        let inner = inner.value.downcast_ref::<T>().unwrap();
-        f(inner)
+    /// Get the scope this value was created in.
+    pub fn origin_scope(&self) -> ScopeId {
+        self.origin_scope
     }
 
-    pub(crate) fn read<T: 'static>(&self, id: usize) -> std::cell::Ref<T> {
-        let signals = self.signals.borrow();
-        std::cell::Ref::map(signals, |signals| {
-            signals[id].value.downcast_ref::<T>().unwrap()
-        })
+    /// Try to read the value. If the value has been dropped, this will return None.
+    pub fn try_read(&self) -> Option<Ref<'_, T>> {
+        self.value.try_read()
     }
 
-    pub(crate) fn write<T: 'static>(&self, id: usize) -> std::cell::RefMut<T> {
-        let signals = self.signals.borrow_mut();
-        std::cell::RefMut::map(signals, |signals| {
-            signals[id].value.downcast_mut::<T>().unwrap()
-        })
+    /// Read the value. If the value has been dropped, this will panic.
+    pub fn read(&self) -> Ref<'_, T> {
+        self.value.read()
     }
 
-    pub(crate) fn getter<T: 'static + Clone>(&self, id: usize) -> &dyn Fn() -> T {
-        let mut signals = self.signals.borrow_mut();
-        let inner = &mut signals[id];
-        let r = inner.getter.as_mut();
+    /// Try to write the value. If the value has been dropped, this will return None.
+    pub fn try_write(&self) -> Option<RefMut<'_, T>> {
+        self.value.try_write()
+    }
 
-        if r.is_none() {
-            let rt = self;
-            let r = move || rt.get::<T>(id);
-            let getter: Box<dyn Fn() -> T> = Box::new(r);
-            let getter: Box<dyn Fn()> = unsafe { std::mem::transmute(getter) };
+    /// Write the value. If the value has been dropped, this will panic.
+    pub fn write(&self) -> RefMut<'_, T> {
+        self.value.write()
+    }
 
-            inner.getter = Some(getter);
-        }
+    /// Set the value. If the value has been dropped, this will panic.
+    pub fn set(&mut self, value: T) {
+        *self.write() = value;
+    }
 
-        let r = inner.getter.as_ref().unwrap();
+    /// Run a function with a reference to the value. If the value has been dropped, this will panic.
+    pub fn with<O>(&self, f: impl FnOnce(&T) -> O) -> O {
+        let write = self.read();
+        f(&*write)
+    }
 
-        unsafe { std::mem::transmute::<&dyn Fn(), &dyn Fn() -> T>(r) }
+    /// Run a function with a mutable reference to the value. If the value has been dropped, this will panic.
+    pub fn with_mut<O>(&self, f: impl FnOnce(&mut T) -> O) -> O {
+        let mut write = self.write();
+        f(&mut *write)
     }
 }
 
-pub(crate) struct Inner {
-    pub value: Box<dyn Any>,
-    pub subscribers: Vec<ScopeId>,
+impl<T: Clone + 'static> CopyValue<T> {
+    /// Get the value. If the value has been dropped, this will panic.
+    pub fn value(&self) -> T {
+        self.read().clone()
+    }
+}
 
-    // todo: this has a soundness hole in it that you might not run into
-    pub getter: Option<Box<dyn Fn()>>,
+impl<T: 'static> PartialEq for CopyValue<T> {
+    fn eq(&self, other: &Self) -> bool {
+        self.value.ptr_eq(&other.value)
+    }
 }

+ 105 - 0
packages/signals/src/selector.rs

@@ -0,0 +1,105 @@
+use dioxus_core::prelude::*;
+
+use crate::dependency::Dependency;
+use crate::use_signal;
+use crate::{get_effect_stack, signal::SignalData, CopyValue, Effect, ReadOnlySignal, Signal};
+
+/// Creates a new Selector. The selector will be run immediately and whenever any signal it reads changes.
+///
+/// Selectors can be used to efficiently compute derived data from signals.
+///
+/// ```rust
+/// use dioxus::prelude::*;
+/// use dioxus_signals::*;
+///
+/// fn App(cx: Scope) -> Element {
+///     let mut count = use_signal(cx, || 0);
+///     let double = use_selector(cx, move || count * 2);
+///     count += 1;
+///     assert_eq!(double.value(), count * 2);
+///  
+///     render! { "{double}" }
+/// }
+/// ```
+pub fn use_selector<R: PartialEq>(
+    cx: &ScopeState,
+    f: impl FnMut() -> R + 'static,
+) -> ReadOnlySignal<R> {
+    *cx.use_hook(|| selector(f))
+}
+
+/// Creates a new Selector with some local dependencies. The selector will be run immediately and whenever any signal it reads or any dependencies it tracks changes
+///
+/// Selectors can be used to efficiently compute derived data from signals.
+///
+/// ```rust
+/// use dioxus::prelude::*;
+/// use dioxus_signals::*;
+///
+/// fn App(cx: Scope) -> Element {
+///     let mut local_state = use_state(cx, || 0);
+///     let double = use_selector_with_dependencies(cx, (local_state.get(),), move |(local_state,)| local_state * 2);
+///     local_state.set(1);
+///  
+///     render! { "{double}" }
+/// }
+/// ```
+pub fn use_selector_with_dependencies<R: PartialEq, D: Dependency>(
+    cx: &ScopeState,
+    dependencies: D,
+    mut f: impl FnMut(D::Out) -> R + 'static,
+) -> ReadOnlySignal<R>
+where
+    D::Out: 'static,
+{
+    let dependencies_signal = use_signal(cx, || dependencies.out());
+    let selector = *cx.use_hook(|| {
+        selector(move || {
+            let deref = &*dependencies_signal.read();
+            f(deref.clone())
+        })
+    });
+    let changed = { dependencies.changed(&*dependencies_signal.read()) };
+    if changed {
+        dependencies_signal.set(dependencies.out());
+    }
+    selector
+}
+
+/// Creates a new Selector. The selector will be run immediately and whenever any signal it reads changes.
+///
+/// Selectors can be used to efficiently compute derived data from signals.
+pub fn selector<R: PartialEq>(mut f: impl FnMut() -> R + 'static) -> ReadOnlySignal<R> {
+    let state = Signal::<R> {
+        inner: CopyValue::invalid(),
+    };
+    let effect = Effect {
+        callback: CopyValue::invalid(),
+    };
+
+    {
+        get_effect_stack().effects.borrow_mut().push(effect);
+    }
+    state.inner.value.set(SignalData {
+        subscribers: Default::default(),
+        effect_subscribers: Default::default(),
+        update_any: schedule_update_any().expect("in a virtual dom"),
+        value: f(),
+    });
+    {
+        get_effect_stack().effects.borrow_mut().pop();
+    }
+
+    effect.callback.value.set(Box::new(move || {
+        let value = f();
+        let changed = {
+            let old = state.inner.read();
+            value != old.value
+        };
+        if changed {
+            state.set(value)
+        }
+    }));
+
+    ReadOnlySignal::new(state)
+}

+ 344 - 0
packages/signals/src/signal.rs

@@ -0,0 +1,344 @@
+use std::{
+    cell::{Ref, RefCell, RefMut},
+    ops::{Deref, DerefMut},
+    rc::Rc,
+    sync::Arc,
+};
+
+use dioxus_core::{
+    prelude::{current_scope_id, has_context, provide_context, schedule_update_any},
+    ScopeId, ScopeState,
+};
+
+use crate::{CopyValue, Effect};
+
+/// Creates a new Signal. Signals are a Copy state management solution with automatic dependency tracking.
+///
+/// ```rust
+/// use dioxus::prelude::*;
+/// use dioxus_signals::*;
+///
+/// fn App(cx: Scope) -> Element {
+///     let mut count = use_signal(cx, || 0);
+///
+///     // Because signals have automatic dependency tracking, if you never read them in a component, that component will not be re-rended when the signal is updated.
+///     // The app component will never be rerendered in this example.
+///     render! { Child { state: count } }
+/// }
+///
+/// #[inline_props]
+/// fn Child(cx: Scope, state: Signal<u32>) -> Element {
+///     let state = *state;
+///
+///     use_future!(cx,  |()| async move {
+///         // Because the signal is a Copy type, we can use it in an async block without cloning it.
+///         *state.write() += 1;
+///     });
+///
+///     render! {
+///         button {
+///             onclick: move |_| *state.write() += 1,
+///             "{state}"
+///         }
+///     }
+/// }
+/// ```
+pub fn use_signal<T: 'static>(cx: &ScopeState, f: impl FnOnce() -> T) -> Signal<T> {
+    *cx.use_hook(|| Signal::new(f()))
+}
+
+#[derive(Clone)]
+struct Unsubscriber {
+    scope: ScopeId,
+    subscribers: UnsubscriberArray,
+}
+
+type UnsubscriberArray = Rc<RefCell<Vec<Rc<RefCell<Vec<ScopeId>>>>>>;
+
+impl Drop for Unsubscriber {
+    fn drop(&mut self) {
+        for subscribers in self.subscribers.borrow().iter() {
+            subscribers.borrow_mut().retain(|s| *s != self.scope);
+        }
+    }
+}
+
+fn current_unsubscriber() -> Unsubscriber {
+    match has_context() {
+        Some(rt) => rt,
+        None => {
+            let owner = Unsubscriber {
+                scope: current_scope_id().expect("in a virtual dom"),
+                subscribers: Default::default(),
+            };
+            provide_context(owner).expect("in a virtual dom")
+        }
+    }
+}
+
+pub(crate) struct SignalData<T> {
+    pub(crate) subscribers: Rc<RefCell<Vec<ScopeId>>>,
+    pub(crate) effect_subscribers: Rc<RefCell<Vec<Effect>>>,
+    pub(crate) update_any: Arc<dyn Fn(ScopeId)>,
+    pub(crate) value: T,
+}
+
+/// Creates a new Signal. Signals are a Copy state management solution with automatic dependency tracking.
+///
+/// ```rust
+/// use dioxus::prelude::*;
+/// use dioxus_signals::*;
+///
+/// fn App(cx: Scope) -> Element {
+///     let mut count = use_signal(cx, || 0);
+///
+///     // Because signals have automatic dependency tracking, if you never read them in a component, that component will not be re-rended when the signal is updated.
+///     // The app component will never be rerendered in this example.
+///     render! { Child { state: count } }
+/// }
+///
+/// #[inline_props]
+/// fn Child(cx: Scope, state: Signal<u32>) -> Element {
+///     let state = *state;
+///
+///     use_future!(cx,  |()| async move {
+///         // Because the signal is a Copy type, we can use it in an async block without cloning it.
+///         *state.write() += 1;
+///     });
+///
+///     render! {
+///         button {
+///             onclick: move |_| *state.write() += 1,
+///             "{state}"
+///         }
+///     }
+/// }
+/// ```
+pub struct Signal<T: 'static> {
+    pub(crate) inner: CopyValue<SignalData<T>>,
+}
+
+#[cfg(feature = "serde")]
+impl<T: serde::Serialize + 'static> serde::Serialize for Signal<T> {
+    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
+        self.read().serialize(serializer)
+    }
+}
+
+#[cfg(feature = "serde")]
+impl<'de, T: serde::Deserialize<'de> + 'static> serde::Deserialize<'de> for Signal<T> {
+    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
+        Ok(Self::new(T::deserialize(deserializer)?))
+    }
+}
+
+impl<T: 'static> Signal<T> {
+    /// Creates a new Signal. Signals are a Copy state management solution with automatic dependency tracking.
+    pub fn new(value: T) -> Self {
+        Self {
+            inner: CopyValue::new(SignalData {
+                subscribers: Default::default(),
+                effect_subscribers: Default::default(),
+                update_any: schedule_update_any().expect("in a virtual dom"),
+                value,
+            }),
+        }
+    }
+
+    /// Get the scope the signal was created in.
+    pub fn origin_scope(&self) -> ScopeId {
+        self.inner.origin_scope()
+    }
+
+    /// Get the current value of the signal. This will subscribe the current scope to the signal.
+    /// If the signal has been dropped, this will panic.
+    pub fn read(&self) -> Ref<T> {
+        let inner = self.inner.read();
+        if let Some(effect) = Effect::current() {
+            let mut effect_subscribers = inner.effect_subscribers.borrow_mut();
+            if !effect_subscribers.contains(&effect) {
+                effect_subscribers.push(effect);
+            }
+        } else if let Some(current_scope_id) = current_scope_id() {
+            // only subscribe if the vdom is rendering
+            if dioxus_core::vdom_is_rendering() {
+                log::trace!(
+                    "{:?} subscribed to {:?}",
+                    self.inner.value,
+                    current_scope_id
+                );
+                let mut subscribers = inner.subscribers.borrow_mut();
+                if !subscribers.contains(&current_scope_id) {
+                    subscribers.push(current_scope_id);
+                    drop(subscribers);
+                    let unsubscriber = current_unsubscriber();
+                    inner.subscribers.borrow_mut().push(unsubscriber.scope);
+                }
+            }
+        }
+        Ref::map(inner, |v| &v.value)
+    }
+
+    /// Get a mutable reference to the signal's value.
+    /// If the signal has been dropped, this will panic.
+    pub fn write(&self) -> Write<'_, T> {
+        let inner = self.inner.write();
+        let borrow = RefMut::map(inner, |v| &mut v.value);
+        Write {
+            write: borrow,
+            signal: SignalSubscriberDrop { signal: *self },
+        }
+    }
+
+    fn update_subscribers(&self) {
+        {
+            let inner = self.inner.read();
+            for &scope_id in &*inner.subscribers.borrow() {
+                log::trace!(
+                    "Write on {:?} triggered update on {:?}",
+                    self.inner.value,
+                    scope_id
+                );
+                (inner.update_any)(scope_id);
+            }
+        }
+
+        let subscribers = {
+            let self_read = self.inner.read();
+            let mut effects = self_read.effect_subscribers.borrow_mut();
+            std::mem::take(&mut *effects)
+        };
+        for effect in subscribers {
+            log::trace!(
+                "Write on {:?} triggered effect {:?}",
+                self.inner.value,
+                effect
+            );
+            effect.try_run();
+        }
+    }
+
+    /// Set the value of the signal. This will trigger an update on all subscribers.
+    pub fn set(&self, value: T) {
+        *self.write() = value;
+    }
+
+    /// Run a closure with a reference to the signal's value.
+    /// If the signal has been dropped, this will panic.
+    pub fn with<O>(&self, f: impl FnOnce(&T) -> O) -> O {
+        let write = self.read();
+        f(&*write)
+    }
+
+    /// Run a closure with a mutable reference to the signal's value.
+    /// If the signal has been dropped, this will panic.
+    pub fn with_mut<O>(&self, f: impl FnOnce(&mut T) -> O) -> O {
+        let mut write = self.write();
+        f(&mut *write)
+    }
+}
+
+impl<T: Clone + 'static> Signal<T> {
+    /// Get the current value of the signal. This will subscribe the current scope to the signal.
+    /// If the signal has been dropped, this will panic.
+    pub fn value(&self) -> T {
+        self.read().clone()
+    }
+}
+
+impl<T: 'static> PartialEq for Signal<T> {
+    fn eq(&self, other: &Self) -> bool {
+        self.inner == other.inner
+    }
+}
+
+struct SignalSubscriberDrop<T: 'static> {
+    signal: Signal<T>,
+}
+
+impl<T: 'static> Drop for SignalSubscriberDrop<T> {
+    fn drop(&mut self) {
+        self.signal.update_subscribers();
+    }
+}
+
+/// A mutable reference to a signal's value.
+pub struct Write<'a, T: 'static, I: 'static = T> {
+    write: RefMut<'a, T>,
+    signal: SignalSubscriberDrop<I>,
+}
+
+impl<'a, T: 'static, I: 'static> Write<'a, T, I> {
+    /// Map the mutable reference to the signal's value to a new type.
+    pub fn map<O>(myself: Self, f: impl FnOnce(&mut T) -> &mut O) -> Write<'a, O, I> {
+        let Self { write, signal } = myself;
+        Write {
+            write: RefMut::map(write, f),
+            signal,
+        }
+    }
+
+    /// Try to map the mutable reference to the signal's value to a new type
+    pub fn filter_map<O>(
+        myself: Self,
+        f: impl FnOnce(&mut T) -> Option<&mut O>,
+    ) -> Option<Write<'a, O, I>> {
+        let Self { write, signal } = myself;
+        let write = RefMut::filter_map(write, f).ok();
+        write.map(|write| Write { write, signal })
+    }
+}
+
+impl<'a, T: 'static> Deref for Write<'a, T> {
+    type Target = T;
+
+    fn deref(&self) -> &Self::Target {
+        &self.write
+    }
+}
+
+impl<T> DerefMut for Write<'_, T> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.write
+    }
+}
+
+/// A signal that can only be read from.
+pub struct ReadOnlySignal<T: 'static> {
+    inner: Signal<T>,
+}
+
+impl<T: 'static> ReadOnlySignal<T> {
+    /// Create a new read-only signal.
+    pub fn new(signal: Signal<T>) -> Self {
+        Self { inner: signal }
+    }
+
+    /// Get the scope that the signal was created in.
+    pub fn origin_scope(&self) -> ScopeId {
+        self.inner.origin_scope()
+    }
+
+    /// Get the current value of the signal. This will subscribe the current scope to the signal.
+    pub fn read(&self) -> Ref<T> {
+        self.inner.read()
+    }
+
+    /// Run a closure with a reference to the signal's value.
+    pub fn with<O>(&self, f: impl FnOnce(&T) -> O) -> O {
+        self.inner.with(f)
+    }
+}
+
+impl<T: Clone + 'static> ReadOnlySignal<T> {
+    /// Get the current value of the signal. This will subscribe the current scope to the signal.
+    pub fn value(&self) -> T {
+        self.read().clone()
+    }
+}
+
+impl<T: 'static> PartialEq for ReadOnlySignal<T> {
+    fn eq(&self, other: &Self) -> bool {
+        self.inner == other.inner
+    }
+}

+ 60 - 0
packages/signals/tests/create.rs

@@ -0,0 +1,60 @@
+#![allow(unused, non_upper_case_globals, non_snake_case)]
+
+use dioxus::prelude::*;
+use dioxus_core::ElementId;
+use dioxus_signals::*;
+
+#[test]
+fn create_signals_global() {
+    let mut dom = VirtualDom::new(|cx| {
+        render! {
+            for _ in 0..10 {
+                Child {}
+            }
+        }
+    });
+
+    fn Child(cx: Scope) -> Element {
+        let signal = create_without_cx();
+
+        render! {
+            "{signal}"
+        }
+    }
+
+    let _edits = dom.rebuild().santize();
+
+    fn create_without_cx() -> Signal<String> {
+        Signal::new("hello world".to_string())
+    }
+}
+
+#[test]
+fn drop_signals() {
+    let mut dom = VirtualDom::new(|cx| {
+        let generation = cx.generation();
+
+        let count = if generation % 2 == 0 { 10 } else { 0 };
+        render! {
+            for _ in 0..count {
+                Child {}
+            }
+        }
+    });
+
+    fn Child(cx: Scope) -> Element {
+        let signal = create_without_cx();
+
+        render! {
+            "{signal}"
+        }
+    }
+
+    let _ = dom.rebuild().santize();
+    dom.mark_dirty(ScopeId(0));
+    dom.render_immediate();
+
+    fn create_without_cx() -> Signal<String> {
+        Signal::new("hello world".to_string())
+    }
+}

+ 47 - 0
packages/signals/tests/effect.rs

@@ -0,0 +1,47 @@
+#![allow(unused, non_upper_case_globals, non_snake_case)]
+use std::collections::HashMap;
+use std::rc::Rc;
+
+use dioxus::prelude::*;
+use dioxus_core::ElementId;
+use dioxus_signals::*;
+
+#[test]
+fn effects_rerun() {
+    simple_logger::SimpleLogger::new().init().unwrap();
+
+    #[derive(Default)]
+    struct RunCounter {
+        component: usize,
+        effect: usize,
+    }
+
+    let counter = Rc::new(RefCell::new(RunCounter::default()));
+    let mut dom = VirtualDom::new_with_props(
+        |cx| {
+            let counter = cx.props;
+            counter.borrow_mut().component += 1;
+
+            let mut signal = use_signal(cx, || 0);
+            cx.use_hook(move || {
+                to_owned![counter];
+                Effect::new(move || {
+                    counter.borrow_mut().effect += 1;
+                    println!("Signal: {:?}", signal);
+                })
+            });
+            signal += 1;
+
+            render! {
+                div {}
+            }
+        },
+        counter.clone(),
+    );
+
+    let _ = dom.rebuild().santize();
+
+    let current_counter = counter.borrow();
+    assert_eq!(current_counter.component, 1);
+    assert_eq!(current_counter.effect, 2);
+}

+ 145 - 0
packages/signals/tests/selector.rs

@@ -0,0 +1,145 @@
+#![allow(unused, non_upper_case_globals, non_snake_case)]
+use std::collections::HashMap;
+use std::rc::Rc;
+
+use dioxus::html::p;
+use dioxus::prelude::*;
+use dioxus_core::ElementId;
+use dioxus_signals::*;
+
+#[test]
+fn memos_rerun() {
+    let _ = simple_logger::SimpleLogger::new().init();
+
+    #[derive(Default)]
+    struct RunCounter {
+        component: usize,
+        effect: usize,
+    }
+
+    let counter = Rc::new(RefCell::new(RunCounter::default()));
+    let mut dom = VirtualDom::new_with_props(
+        |cx| {
+            let counter = cx.props;
+            counter.borrow_mut().component += 1;
+
+            let mut signal = use_signal(cx, || 0);
+            let memo = cx.use_hook(move || {
+                to_owned![counter];
+                selector(move || {
+                    counter.borrow_mut().effect += 1;
+                    println!("Signal: {:?}", signal);
+                    signal.value()
+                })
+            });
+            assert_eq!(memo.value(), 0);
+            signal += 1;
+            assert_eq!(memo.value(), 1);
+
+            render! {
+                div {}
+            }
+        },
+        counter.clone(),
+    );
+
+    let _ = dom.rebuild().santize();
+
+    let current_counter = counter.borrow();
+    assert_eq!(current_counter.component, 1);
+    assert_eq!(current_counter.effect, 2);
+}
+
+#[test]
+fn memos_prevents_component_rerun() {
+    let _ = simple_logger::SimpleLogger::new().init();
+
+    #[derive(Default)]
+    struct RunCounter {
+        component: usize,
+        effect: usize,
+    }
+
+    let counter = Rc::new(RefCell::new(RunCounter::default()));
+    let mut dom = VirtualDom::new_with_props(
+        |cx| {
+            let mut signal = use_signal(cx, || 0);
+
+            if cx.generation() == 1 {
+                *signal.write() = 0;
+            }
+            if cx.generation() == 2 {
+                println!("Writing to signal");
+                *signal.write() = 1;
+            }
+
+            render! {
+                Child {
+                    signal: signal,
+                    counter: cx.props.clone(),
+                }
+            }
+        },
+        counter.clone(),
+    );
+
+    #[derive(Default, Props)]
+    struct ChildProps {
+        signal: Signal<usize>,
+        counter: Rc<RefCell<RunCounter>>,
+    }
+
+    impl PartialEq for ChildProps {
+        fn eq(&self, other: &Self) -> bool {
+            self.signal == other.signal
+        }
+    }
+
+    fn Child(cx: Scope<ChildProps>) -> Element {
+        let counter = &cx.props.counter;
+        let signal = cx.props.signal;
+        counter.borrow_mut().component += 1;
+
+        let memo = cx.use_hook(move || {
+            to_owned![counter];
+            selector(move || {
+                counter.borrow_mut().effect += 1;
+                println!("Signal: {:?}", signal);
+                signal.value()
+            })
+        });
+        match cx.generation() {
+            0 => {
+                assert_eq!(memo.value(), 0);
+            }
+            1 => {
+                assert_eq!(memo.value(), 1);
+            }
+            _ => panic!("Unexpected generation"),
+        }
+
+        render! {
+            div {}
+        }
+    }
+
+    let _ = dom.rebuild().santize();
+    dom.mark_dirty(ScopeId(0));
+    dom.render_immediate();
+
+    {
+        let current_counter = counter.borrow();
+        assert_eq!(current_counter.component, 1);
+        assert_eq!(current_counter.effect, 2);
+    }
+
+    dom.mark_dirty(ScopeId(0));
+    dom.render_immediate();
+    dom.render_immediate();
+
+    {
+        let current_counter = counter.borrow();
+        assert_eq!(current_counter.component, 2);
+        assert_eq!(current_counter.effect, 3);
+    }
+}

+ 92 - 0
packages/signals/tests/subscribe.rs

@@ -0,0 +1,92 @@
+#![allow(unused, non_upper_case_globals, non_snake_case)]
+use std::collections::HashMap;
+use std::rc::Rc;
+
+use dioxus::prelude::*;
+use dioxus_core::ElementId;
+use dioxus_signals::*;
+
+#[test]
+fn reading_subscribes() {
+    simple_logger::SimpleLogger::new().init().unwrap();
+
+    #[derive(Default)]
+    struct RunCounter {
+        parent: usize,
+        children: HashMap<ScopeId, usize>,
+    }
+
+    let counter = Rc::new(RefCell::new(RunCounter::default()));
+    let mut dom = VirtualDom::new_with_props(
+        |cx| {
+            let mut signal = use_signal(cx, || 0);
+
+            println!("Parent: {:?}", cx.scope_id());
+            if cx.generation() == 1 {
+                signal += 1;
+            }
+
+            cx.props.borrow_mut().parent += 1;
+
+            render! {
+                for id in 0..10 {
+                    Child {
+                        signal: signal,
+                        counter: cx.props.clone()
+                    }
+                }
+            }
+        },
+        counter.clone(),
+    );
+
+    #[derive(Props, Clone)]
+    struct ChildProps {
+        signal: Signal<usize>,
+        counter: Rc<RefCell<RunCounter>>,
+    }
+
+    impl PartialEq for ChildProps {
+        fn eq(&self, other: &Self) -> bool {
+            self.signal == other.signal
+        }
+    }
+
+    fn Child(cx: Scope<ChildProps>) -> Element {
+        println!("Child: {:?}", cx.scope_id());
+        *cx.props
+            .counter
+            .borrow_mut()
+            .children
+            .entry(cx.scope_id())
+            .or_default() += 1;
+
+        render! {
+            "{cx.props.signal}"
+        }
+    }
+
+    let _ = dom.rebuild().santize();
+
+    {
+        let current_counter = counter.borrow();
+        assert_eq!(current_counter.parent, 1);
+
+        for (scope_id, rerun_count) in current_counter.children.iter() {
+            assert_eq!(rerun_count, &1);
+        }
+    }
+
+    dom.mark_dirty(ScopeId(0));
+    dom.render_immediate();
+    dom.render_immediate();
+
+    {
+        let current_counter = counter.borrow();
+        assert_eq!(current_counter.parent, 2);
+
+        for (scope_id, rerun_count) in current_counter.children.iter() {
+            assert_eq!(rerun_count, &2);
+        }
+    }
+}