Explorar o código

Merge branch 'main' into jk/fix-form-inputs

Jonathan Kelley hai 1 ano
pai
achega
16b38e339d

+ 15 - 2
Cargo.lock

@@ -2361,6 +2361,8 @@ dependencies = [
  "slab",
  "tokio",
  "tracing",
+ "tracing-fluent-assertions",
+ "tracing-subscriber",
 ]
 
 [[package]]
@@ -5924,9 +5926,9 @@ dependencies = [
 
 [[package]]
 name = "mio"
-version = "0.8.10"
+version = "0.8.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09"
+checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
 dependencies = [
  "libc",
  "log",
@@ -9705,6 +9707,17 @@ dependencies = [
  "valuable",
 ]
 
+[[package]]
+name = "tracing-fluent-assertions"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "12de1a8c6bcfee614305e836308b596bbac831137a04c61f7e5b0b0bf2cfeaf6"
+dependencies = [
+ "tracing",
+ "tracing-core",
+ "tracing-subscriber",
+]
+
 [[package]]
 name = "tracing-futures"
 version = "0.2.5"

+ 2 - 2
examples/future.rs

@@ -1,7 +1,7 @@
 //! A simple example that shows how to use the use_future hook to run a background task.
 //!
-//! use_future assumes your future will never complete - it won't return a value.
-//! If you want to return a value, use use_resource instead.
+//! use_future won't return a value, analagous to use_effect.
+//! If you want to return a value from a future, use use_resource instead.
 
 use dioxus::prelude::*;
 use std::time::Duration;

+ 1 - 1
packages/core-macro/src/lib.rs

@@ -167,7 +167,7 @@ pub(crate) const COMPONENT_ARG_CASE_CHECK_OFF: &str = "no_case_check";
 ///     #[warn(non_snake_case)]
 ///     #[inline(always)]
 ///     fn __dx_inner_comp(props: GreetPersonProps>e) -> Element {
-///         let GreetPersonProps { person } = &cx.props;
+///         let GreetPersonProps { person } = props;
 ///         {
 ///             rsx! { "hello, {person}" }
 ///         }

+ 2 - 0
packages/core/Cargo.toml

@@ -20,9 +20,11 @@ slab = { workspace = true }
 futures-channel = { workspace = true }
 tracing = { workspace = true }
 serde = { version = "1", features = ["derive"], optional = true }
+tracing-subscriber = "0.3.18"
 
 [dev-dependencies]
 tokio = { workspace = true, features = ["full"] }
+tracing-fluent-assertions = "0.3.0"
 dioxus = { workspace = true }
 pretty_assertions = "1.3.0"
 rand = "0.8.5"

+ 1 - 5
packages/core/src/diff/component.rs

@@ -79,11 +79,7 @@ impl VNode {
         // The target ScopeState still has the reference to the old props, so there's no need to update anything
         // This also implicitly drops the new props since they're not used
         if old_props.memoize(new_props.props()) {
-            tracing::trace!(
-                "Memoized props for component {:#?} ({})",
-                scope_id,
-                old_scope.state().name
-            );
+            tracing::trace!("Memoized props for component {:#?}", scope_id,);
             return;
         }
 

+ 19 - 2
packages/core/src/scopes.rs

@@ -1,7 +1,7 @@
 use crate::{
     any_props::BoxedAnyProps, nodes::RenderReturn, runtime::Runtime, scope_context::Scope,
 };
-use std::{cell::Ref, fmt::Debug, rc::Rc};
+use std::{cell::Ref, rc::Rc};
 
 /// A component's unique identifier.
 ///
@@ -9,9 +9,26 @@ use std::{cell::Ref, fmt::Debug, rc::Rc};
 /// time. We do try and guarantee that between calls to `wait_for_work`, no ScopeIds will be recycled in order to give
 /// time for any logic that relies on these IDs to properly update.
 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
-#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
+#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
 pub struct ScopeId(pub usize);
 
+impl std::fmt::Debug for ScopeId {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let mut builder = f.debug_tuple("ScopeId");
+        let mut builder = builder.field(&self.0);
+        #[cfg(debug_assertions)]
+        {
+            if let Some(name) = Runtime::current()
+                .as_ref()
+                .and_then(|rt| rt.get_state(*self))
+            {
+                builder = builder.field(&name.name);
+            }
+        }
+        builder.finish()
+    }
+}
+
 impl ScopeId {
     /// The root ScopeId.
     ///

+ 37 - 4
packages/core/src/virtual_dom.rs

@@ -19,6 +19,7 @@ use futures_util::StreamExt;
 use rustc_hash::{FxHashMap, FxHashSet};
 use slab::Slab;
 use std::{any::Any, collections::BTreeSet, rc::Rc};
+use tracing::instrument;
 
 /// A virtual node system that progresses user events and diffs UI trees.
 ///
@@ -303,6 +304,7 @@ impl VirtualDom {
     /// let mut dom = VirtualDom::new_from_root(VComponent::new(Example, SomeProps { name: "jane" }, "Example"));
     /// let mutations = dom.rebuild();
     /// ```
+    #[instrument(skip(root), level = "trace", name = "VirtualDom::new")]
     pub(crate) fn new_with_component(root: impl AnyProps + 'static) -> Self {
         let (tx, rx) = futures_channel::mpsc::unbounded();
 
@@ -345,6 +347,7 @@ impl VirtualDom {
     }
 
     /// Run a closure inside the dioxus runtime
+    #[instrument(skip(self, f), level = "trace", name = "VirtualDom::in_runtime")]
     pub fn in_runtime<O>(&self, f: impl FnOnce() -> O) -> O {
         let _runtime = RuntimeGuard::new(self.runtime.clone());
         f()
@@ -373,7 +376,13 @@ impl VirtualDom {
             return;
         };
 
-        tracing::trace!("Marking scope {:?} ({}) as dirty", id, scope.name);
+        tracing::event!(
+            tracing::Level::TRACE,
+            "Marking scope {:?} ({}) as dirty",
+            id,
+            scope.name
+        );
+
         self.dirty_scopes.insert(DirtyScope {
             height: scope.height(),
             id,
@@ -389,6 +398,7 @@ impl VirtualDom {
     /// It is up to the listeners themselves to mark nodes as dirty.
     ///
     /// If you have multiple events, you can call this method multiple times before calling "render_with_deadline"
+    #[instrument(skip(self), level = "trace", name = "VirtualDom::handle_event")]
     pub fn handle_event(
         &mut self,
         name: &str,
@@ -422,12 +432,14 @@ impl VirtualDom {
     /// ```rust, ignore
     /// let dom = VirtualDom::new(app);
     /// ```
+    #[instrument(skip(self), level = "trace", name = "VirtualDom::wait_for_work")]
     pub async fn wait_for_work(&mut self) {
         // And then poll the futures
         self.poll_tasks().await;
     }
 
     ///
+    #[instrument(skip(self), level = "trace", name = "VirtualDom::poll_tasks")]
     async fn poll_tasks(&mut self) {
         // Release the flush lock
         // This will cause all the flush wakers to immediately spring to life, which we will off with process_events
@@ -461,6 +473,7 @@ impl VirtualDom {
     }
 
     /// Process all events in the queue until there are no more left
+    #[instrument(skip(self), level = "trace", name = "VirtualDom::process_events")]
     pub fn process_events(&mut self) {
         let _runtime = RuntimeGuard::new(self.runtime.clone());
 
@@ -478,7 +491,8 @@ impl VirtualDom {
     ///
     /// The caller must ensure that the template references the same dynamic attributes and nodes as the original template.
     ///
-    /// This will only replace the the parent template, not any nested templates.
+    /// This will only replace the parent template, not any nested templates.
+    #[instrument(skip(self), level = "trace", name = "VirtualDom::replace_template")]
     pub fn replace_template(&mut self, template: Template) {
         self.register_template_first_byte_index(template);
         // iterating a slab is very inefficient, but this is a rare operation that will only happen during development so it's fine
@@ -518,7 +532,7 @@ impl VirtualDom {
     /// The mutations item expects the RealDom's stack to be the root of the application.
     ///
     /// Tasks will not be polled with this method, nor will any events be processed from the event queue. Instead, the
-    /// root component will be ran once and then diffed. All updates will flow out as mutations.
+    /// root component will be run once and then diffed. All updates will flow out as mutations.
     ///
     /// All state stored in components will be completely wiped away.
     ///
@@ -533,6 +547,7 @@ impl VirtualDom {
     ///
     /// apply_edits(edits);
     /// ```
+    #[instrument(skip(self, to), level = "trace", name = "VirtualDom::rebuild")]
     pub fn rebuild(&mut self, to: &mut impl WriteMutations) {
         self.flush_templates(to);
         let _runtime = RuntimeGuard::new(self.runtime.clone());
@@ -546,6 +561,7 @@ impl VirtualDom {
 
     /// Render whatever the VirtualDom has ready as fast as possible without requiring an executor to progress
     /// suspended subtrees.
+    #[instrument(skip(self, to), level = "trace", name = "VirtualDom::render_immediate")]
     pub fn render_immediate(&mut self, to: &mut impl WriteMutations) {
         self.flush_templates(to);
 
@@ -584,7 +600,8 @@ impl VirtualDom {
     /// The mutations will be thrown out, so it's best to use this method for things like SSR that have async content
     ///
     /// We don't call "flush_sync" here since there's no sync work to be done. Futures will be progressed like usual,
-    /// however any futures wating on flush_sync will remain pending
+    /// however any futures waiting on flush_sync will remain pending
+    #[instrument(skip(self), level = "trace", name = "VirtualDom::wait_for_suspense")]
     pub async fn wait_for_suspense(&mut self) {
         loop {
             if self.suspended_scopes.is_empty() {
@@ -605,6 +622,7 @@ impl VirtualDom {
     }
 
     /// Flush any queued template changes
+    #[instrument(skip(self, to), level = "trace", name = "VirtualDom::flush_templates")]
     fn flush_templates(&mut self, to: &mut impl WriteMutations) {
         for template in self.queued_templates.drain(..) {
             to.register_template(template);
@@ -632,6 +650,11 @@ impl VirtualDom {
     | | |       <-- no, broke early
     |           <-- no, broke early
     */
+    #[instrument(
+        skip(self, uievent),
+        level = "trace",
+        name = "VirtualDom::handle_bubbling_event"
+    )]
     fn handle_bubbling_event(
         &mut self,
         mut parent: Option<ElementRef>,
@@ -670,6 +693,11 @@ impl VirtualDom {
 
             // Now that we've accumulated all the parent attributes for the target element, call them in reverse order
             // We check the bubble state between each call to see if the event has been stopped from bubbling
+            tracing::event!(
+                tracing::Level::TRACE,
+                "Calling {} listeners",
+                listeners.len()
+            );
             for listener in listeners.into_iter().rev() {
                 if let AttributeValue::Listener(listener) = listener {
                     self.runtime.rendering.set(false);
@@ -688,6 +716,11 @@ impl VirtualDom {
     }
 
     /// Call an event listener in the simplest way possible without bubbling upwards
+    #[instrument(
+        skip(self, uievent),
+        level = "trace",
+        name = "VirtualDom::handle_non_bubbling_event"
+    )]
     fn handle_non_bubbling_event(&mut self, node: ElementRef, name: &str, uievent: Event<dyn Any>) {
         let el_ref = &self.mounts[node.mount.0].node;
         let node_template = el_ref.template.get();

+ 82 - 0
packages/core/tests/tracing.rs

@@ -0,0 +1,82 @@
+use dioxus::html::SerializedHtmlEventConverter;
+use dioxus::prelude::*;
+use dioxus_core::ElementId;
+use std::rc::Rc;
+use tracing_fluent_assertions::{AssertionRegistry, AssertionsLayer};
+use tracing_subscriber::{layer::SubscriberExt, Registry};
+
+#[test]
+fn basic_tracing() {
+    // setup tracing
+    let assertion_registry = AssertionRegistry::default();
+    let base_subscriber = Registry::default();
+    // log to standard out for testing
+    let std_out_log = tracing_subscriber::fmt::layer().pretty();
+    let subscriber = base_subscriber
+        .with(std_out_log)
+        .with(AssertionsLayer::new(&assertion_registry));
+    tracing::subscriber::set_global_default(subscriber).unwrap();
+
+    let new_virtual_dom = assertion_registry
+        .build()
+        .with_name("VirtualDom::new")
+        .was_created()
+        .was_entered_exactly(1)
+        .was_closed()
+        .finalize();
+
+    let edited_virtual_dom = assertion_registry
+        .build()
+        .with_name("VirtualDom::rebuild")
+        .was_created()
+        .was_entered_exactly(1)
+        .was_closed()
+        .finalize();
+
+    set_event_converter(Box::new(SerializedHtmlEventConverter));
+    let mut dom = VirtualDom::new(app);
+
+    dom.rebuild(&mut dioxus_core::NoOpMutations);
+
+    new_virtual_dom.assert();
+    edited_virtual_dom.assert();
+
+    for _ in 0..3 {
+        dom.handle_event(
+            "click",
+            Rc::new(PlatformEventData::new(Box::<SerializedMouseData>::default())),
+            ElementId(2),
+            true,
+        );
+        dom.process_events();
+        _ = dom.render_immediate_to_vec();
+    }
+}
+
+fn app() -> Element {
+    let mut idx = use_signal(|| 0);
+    let onhover = |_| println!("go!");
+
+    rsx! {
+        div {
+            button {
+                onclick: move |_| {
+                    idx += 1;
+                    println!("Clicked");
+                },
+                "+"
+            }
+            button { onclick: move |_| idx -= 1, "-" }
+            ul {
+                {(0..idx()).map(|i| rsx! {
+                    ChildExample { i: i, onhover: onhover }
+                })}
+            }
+        }
+    }
+}
+
+#[component]
+fn ChildExample(i: i32, onhover: EventHandler<MouseEvent>) -> Element {
+    rsx! { li { onmouseover: move |e| onhover.call(e), "{i}" } }
+}

+ 3 - 2
packages/fullstack/src/render.rs

@@ -9,6 +9,7 @@ use dioxus_ssr::{
 use std::future::Future;
 use std::sync::Arc;
 use std::sync::RwLock;
+use tokio::task::block_in_place;
 use tokio::task::JoinHandle;
 
 use crate::prelude::*;
@@ -64,7 +65,7 @@ impl SsrRendererPool {
                     let prev_context = SERVER_CONTEXT.with(|ctx| ctx.replace(server_context));
                     // poll the future, which may call server_context()
                     tracing::info!("Rebuilding vdom");
-                    vdom.rebuild(&mut NoOpMutations);
+                    block_in_place(|| vdom.rebuild(&mut NoOpMutations));
                     vdom.wait_for_suspense().await;
                     tracing::info!("Suspense resolved");
                     // after polling the future, we need to restore the context
@@ -124,7 +125,7 @@ impl SsrRendererPool {
                                         .with(|ctx| ctx.replace(Box::new(server_context)));
                                     // poll the future, which may call server_context()
                                     tracing::info!("Rebuilding vdom");
-                                    vdom.rebuild(&mut NoOpMutations);
+                                    block_in_place(|| vdom.rebuild(&mut NoOpMutations));
                                     vdom.wait_for_suspense().await;
                                     tracing::info!("Suspense resolved");
                                     // after polling the future, we need to restore the context

+ 25 - 3
packages/fullstack/src/server_context.rs

@@ -84,14 +84,36 @@ mod server_fn_impl {
         /// Get the request that triggered:
         /// - The initial SSR render if called from a ScopeState or ServerFn
         /// - The server function to be called if called from a server function after the initial render
-        pub fn request_parts(&self) -> tokio::sync::RwLockReadGuard<'_, http::request::Parts> {
+        pub async fn request_parts(
+            &self,
+        ) -> tokio::sync::RwLockReadGuard<'_, http::request::Parts> {
+            self.parts.read().await
+        }
+
+        /// Get the request that triggered:
+        /// - The initial SSR render if called from a ScopeState or ServerFn
+        /// - The server function to be called if called from a server function after the initial render
+        pub fn request_parts_blocking(
+            &self,
+        ) -> tokio::sync::RwLockReadGuard<'_, http::request::Parts> {
             self.parts.blocking_read()
         }
 
         /// Get the request that triggered:
         /// - The initial SSR render if called from a ScopeState or ServerFn
         /// - The server function to be called if called from a server function after the initial render
-        pub fn request_parts_mut(&self) -> tokio::sync::RwLockWriteGuard<'_, http::request::Parts> {
+        pub async fn request_parts_mut(
+            &self,
+        ) -> tokio::sync::RwLockWriteGuard<'_, http::request::Parts> {
+            self.parts.write().await
+        }
+
+        /// Get the request that triggered:
+        /// - The initial SSR render if called from a ScopeState or ServerFn
+        /// - The server function to be called if called from a server function after the initial render
+        pub fn request_parts_mut_blocking(
+            &self,
+        ) -> tokio::sync::RwLockWriteGuard<'_, http::request::Parts> {
             self.parts.blocking_write()
         }
 
@@ -239,6 +261,6 @@ impl<
     type Rejection = R;
 
     async fn from_request(req: &DioxusServerContext) -> Result<Self, Self::Rejection> {
-        Ok(I::from_request_parts(&mut req.request_parts_mut(), &()).await?)
+        Ok(I::from_request_parts(&mut *req.request_parts_mut().await, &()).await?)
     }
 }

+ 30 - 0
packages/hooks/src/use_context.rs

@@ -14,6 +14,18 @@ pub fn try_use_context<T: 'static + Clone>() -> Option<T> {
 /// Consume some context in the tree, providing a sharable handle to the value
 ///
 /// Does not regenerate the value if the value is changed at the parent.
+/// ```rust
+/// fn Parent() -> Element {
+///     use_context_provider(|| Theme::Dark);
+///     rsx! { Child {} }
+/// }
+/// #[component]
+/// fn Child() -> Element {
+///     //gets context provided by parent element with use_context_provider
+///     let user_theme = use_context::<Theme>();
+///     rsx! { "user using dark mode: {user_theme == Theme::Dark}" }
+/// }
+/// ```
 #[must_use]
 pub fn use_context<T: 'static + Clone>() -> T {
     use_hook(|| consume_context::<T>())
@@ -22,6 +34,24 @@ pub fn use_context<T: 'static + Clone>() -> T {
 /// Provide some context via the tree and return a reference to it
 ///
 /// Once the context has been provided, it is immutable. Mutations should be done via interior mutability.
+/// Context can be read by any child components of the context provider, and is a solution to prop
+/// drilling, using a context provider with a Signal inside is a good way to provide global/shared
+/// state in your app:
+/// ```rust
+///fn app() -> Element {
+///    use_context_provider(|| Signal::new(0));
+///    rsx! { Child {} }
+///}
+/// // This component does read from the signal, so when the signal changes it will rerun
+///#[component]
+///fn Child() -> Element {
+///     let signal: Signal<i32> = use_context();
+///     rsx! {
+///         button { onclick: move |_| signal += 1, "increment context" }
+///         p {"{signal}"}
+///     }
+///}
+/// ```
 pub fn use_context_provider<T: 'static + Clone>(f: impl FnOnce() -> T) -> T {
     use_hook(|| {
         let val = f();

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

@@ -93,7 +93,8 @@ where
 }
 
 /// Get a handle to a coroutine higher in the tree
-///
+/// Analagous to use_context_provider and use_context,
+/// but used for coroutines specifically
 /// See the docs for [`use_coroutine`] for more details.
 #[must_use]
 pub fn use_coroutine_handle<M: 'static>() -> Coroutine<M> {

+ 20 - 6
packages/hooks/src/use_effect.rs

@@ -1,21 +1,35 @@
 use dioxus_core::prelude::*;
 use dioxus_signals::ReactiveContext;
 
-/// 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.
-///
+/// `use_effect` will subscribe to any changes in the signal values it captures
+/// effects will always run after first mount and then whenever the signal values change
 /// If the use_effect call was skipped due to an early return, the effect will no longer activate.
+/// ```rust
+/// fn app() -> Element {
+///     let mut count = use_signal(|| 0);
+///     //the effect runs again each time count changes
+///     use_effect(move || println!("Count changed to {count}"));
+///
+///     rsx! {
+///         h1 { "High-Five counter: {count}" }
+///         button { onclick: move |_| count += 1, "Up high!" }
+///         button { onclick: move |_| count -= 1, "Down low!" }
+///     }
+/// }
+/// ```
+#[track_caller]
 pub fn use_effect(mut callback: impl FnMut() + 'static) {
     // let mut run_effect = use_hook(|| CopyValue::new(true));
     // use_hook_did_run(move |did_run| run_effect.set(did_run));
 
+    let location = std::panic::Location::caller();
+
     use_hook(|| {
         spawn(async move {
-            let rc = ReactiveContext::new();
-
+            let rc = ReactiveContext::new_with_origin(location);
             loop {
                 // Wait for the dom the be finished with sync work
-                flush_sync().await;
+                // flush_sync().await;
 
                 // Run the effect
                 rc.run_in(&mut callback);

+ 26 - 2
packages/hooks/src/use_future.rs

@@ -8,10 +8,34 @@ use dioxus_signals::*;
 use dioxus_signals::{Readable, Writable};
 use std::future::Future;
 
-/// A hook that allows you to spawn a future
-///
+/// A hook that allows you to spawn a future.
+/// This future will **not** run on the server
 /// The future is spawned on the next call to `flush_sync` which means that it will not run on the server.
 /// To run a future on the server, you should use `spawn` directly.
+/// `use_future` **won't return a value**.
+/// If you want to return a value from a future, use `use_resource` instead.
+/// ```rust
+/// fn app() -> Element {
+///     let mut count = use_signal(|| 0);
+///     let mut running = use_signal(|| true);
+///     // `use_future` will spawn an infinitely running future that can be started and stopped
+///     use_future(move || async move {
+///         loop {
+///            if running() {
+///                count += 1;
+///            }
+///            tokio::time::sleep(Duration::from_millis(400)).await;
+///        }
+///     });
+///     rsx! {
+///         div {
+///             h1 { "Current count: {count}" }
+///             button { onclick: move |_| running.toggle(), "Start/Stop the count"}
+///             button { onclick: move |_| count.set(0), "Reset the count" }
+///         }
+///     }
+/// }
+/// ```
 pub fn use_future<F>(mut future: impl FnMut() -> F + 'static) -> UseFuture
 where
     F: Future + 'static,

+ 25 - 1
packages/hooks/src/use_resource.rs

@@ -10,8 +10,32 @@ use futures_util::{future, pin_mut, FutureExt};
 use std::future::Future;
 
 /// A memo that resolve to a value asynchronously.
+/// Unlike `use_future`, `use_resource` runs on the **server**
+/// See [`Resource`] for more details.
+/// ```rust
+///fn app() -> Element {
+///    let country = use_signal(|| WeatherLocation {
+///        city: "Berlin".to_string(),
+///        country: "Germany".to_string(),
+///        coordinates: (52.5244, 13.4105)
+///    });
 ///
-/// This runs on the server
+///    let current_weather = //run a future inside the use_resource hook
+///        use_resource(move || async move { get_weather(&country.read().clone()).await });
+///    
+///    rsx! {
+///        //the value of the future can be polled to
+///        //conditionally render elements based off if the future
+///        //finished (Some(Ok(_)), errored Some(Err(_)),
+///        //or is still finishing (None)
+///        match current_weather.value() {
+///            Some(Ok(weather)) => WeatherElement { weather },
+///            Some(Err(e)) => p { "Loading weather failed, {e}" }
+///            None =>  p { "Loading..." }
+///        }
+///    }
+///}
+/// ```
 #[must_use = "Consider using `cx.spawn` to run a future without reading its value"]
 pub fn use_resource<T, F>(future: impl Fn() -> F + 'static) -> Resource<T>
 where

+ 4 - 4
packages/html/src/elements.rs

@@ -1613,10 +1613,10 @@ builder_constructors! {
     /// element.
     hatchpath "http://www.w3.org/2000/svg" {};
 
-    // /// Build a
-    // /// [`<image>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/image)
-    // /// element.
-    // image "http://www.w3.org/2000/svg" {};
+    /// Build a
+    /// [`<image>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/image)
+    /// element.
+    image "http://www.w3.org/2000/svg" {};
 
     /// Build a
     /// [`<line>`](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/line)

+ 604 - 0
packages/interpreter/src/interpreter.js

@@ -145,3 +145,607 @@ this.handler = async function (event, name, bubbles) {
     );
   }
 }
+
+function find_real_id(target) {
+  let realId = null;
+  if (target instanceof Element) {
+    realId = target.getAttribute(`data-dioxus-id`);
+  }
+  // walk the tree to find the real element
+  while (realId == null) {
+    // we've reached the root we don't want to send an event
+    if (target.parentElement === null) {
+      return;
+    }
+
+    target = target.parentElement;
+    if (target instanceof Element) {
+      realId = target.getAttribute(`data-dioxus-id`);
+    }
+  }
+  return realId;
+}
+
+class ListenerMap {
+  constructor(root) {
+    // bubbling events can listen at the root element
+    this.global = {};
+    // non bubbling events listen at the element the listener was created at
+    this.local = {};
+    this.root = null;
+  }
+
+  create(event_name, element, bubbles, handler) {
+    if (bubbles) {
+      if (this.global[event_name] === undefined) {
+        this.global[event_name] = {};
+        this.global[event_name].active = 1;
+        this.root.addEventListener(event_name, handler);
+      } else {
+        this.global[event_name].active++;
+      }
+    }
+    else {
+      const id = element.getAttribute("data-dioxus-id");
+      if (!this.local[id]) {
+        this.local[id] = {};
+      }
+      element.addEventListener(event_name, handler);
+    }
+  }
+
+  remove(element, event_name, bubbles) {
+    if (bubbles) {
+      this.global[event_name].active--;
+      if (this.global[event_name].active === 0) {
+        this.root.removeEventListener(event_name, this.global[event_name].callback);
+        delete this.global[event_name];
+      }
+    }
+    else {
+      const id = element.getAttribute("data-dioxus-id");
+      delete this.local[id][event_name];
+      if (this.local[id].length === 0) {
+        delete this.local[id];
+      }
+      element.removeEventListener(event_name, this.global[event_name].callback);
+    }
+  }
+
+  removeAllNonBubbling(element) {
+    const id = element.getAttribute("data-dioxus-id");
+    delete this.local[id];
+  }
+}
+this.LoadChild = function (array) {
+  // iterate through each number and get that child
+  let node = this.stack[this.stack.length - 1];
+
+  for (let i = 0; i < array.length; i++) {
+    this.end = array[i];
+    for (node = node.firstChild; this.end > 0; this.end--) {
+      node = node.nextSibling;
+    }
+  }
+  return node;
+}
+this.listeners = new ListenerMap();
+this.nodes = [];
+this.stack = [];
+this.templates = {};
+this.end = null;
+
+this.AppendChildren = function (id, many) {
+  let root = this.nodes[id];
+  let els = this.stack.splice(this.stack.length - many);
+  for (let k = 0; k < many; k++) {
+    root.appendChild(els[k]);
+  }
+}
+
+this.initialize = function (root) {
+  this.nodes = [root];
+  this.stack = [root];
+  this.listeners.root = root;
+}
+
+this.getClientRect = function (id) {
+  const node = this.nodes[id];
+  if (!node) {
+    return;
+  }
+  const rect = node.getBoundingClientRect();
+  return {
+    type: "GetClientRect",
+    origin: [rect.x, rect.y],
+    size: [rect.width, rect.height],
+  };
+}
+
+this.scrollTo = function (id, behavior) {
+  const node = this.nodes[id];
+  if (!node) {
+    return false;
+  }
+  node.scrollIntoView({
+    behavior: behavior,
+  });
+  return true;
+}
+
+/// Set the focus on the element
+this.setFocus = function (id, focus) {
+  const node = this.nodes[id];
+  if (!node) {
+    return false;
+  }
+  if (focus) {
+    node.focus();
+  } else {
+    node.blur();
+  }
+  return true;
+}
+
+function get_mouse_data(event) {
+  const {
+    altKey,
+    button,
+    buttons,
+    clientX,
+    clientY,
+    ctrlKey,
+    metaKey,
+    offsetX,
+    offsetY,
+    pageX,
+    pageY,
+    screenX,
+    screenY,
+    shiftKey,
+  } = event;
+  return {
+    alt_key: altKey,
+    button: button,
+    buttons: buttons,
+    client_x: clientX,
+    client_y: clientY,
+    ctrl_key: ctrlKey,
+    meta_key: metaKey,
+    offset_x: offsetX,
+    offset_y: offsetY,
+    page_x: pageX,
+    page_y: pageY,
+    screen_x: screenX,
+    screen_y: screenY,
+    shift_key: shiftKey,
+  };
+}
+
+async function serialize_event(event) {
+  switch (event.type) {
+    case "copy":
+    case "cut":
+    case "past": {
+      return {};
+    }
+    case "compositionend":
+    case "compositionstart":
+    case "compositionupdate": {
+      let { data } = event;
+      return {
+        data,
+      };
+    }
+    case "keydown":
+    case "keypress":
+    case "keyup": {
+      let {
+        charCode,
+        isComposing,
+        key,
+        altKey,
+        ctrlKey,
+        metaKey,
+        keyCode,
+        shiftKey,
+        location,
+        repeat,
+        which,
+        code,
+      } = event;
+      return {
+        char_code: charCode,
+        is_composing: isComposing,
+        key: key,
+        alt_key: altKey,
+        ctrl_key: ctrlKey,
+        meta_key: metaKey,
+        key_code: keyCode,
+        shift_key: shiftKey,
+        location: location,
+        repeat: repeat,
+        which: which,
+        code,
+      };
+    }
+    case "focus":
+    case "blur": {
+      return {};
+    }
+    case "change": {
+      let target = event.target;
+      let value;
+      if (target.type === "checkbox" || target.type === "radio") {
+        value = target.checked ? "true" : "false";
+      } else {
+        value = target.value ?? target.textContent;
+      }
+      return {
+        value: value,
+        values: {},
+      };
+    }
+    case "input":
+    case "invalid":
+    case "reset":
+    case "submit": {
+      let target = event.target;
+      let value = target.value ?? target.textContent;
+
+      if (target.type === "checkbox") {
+        value = target.checked ? "true" : "false";
+      }
+
+      return {
+        value: value,
+        values: {},
+      };
+    }
+    case "drag":
+    case "dragend":
+    case "dragenter":
+    case "dragexit":
+    case "dragleave":
+    case "dragover":
+    case "dragstart":
+    case "drop": {
+      let files = null;
+      if (event.dataTransfer && event.dataTransfer.files) {
+        files = await serializeFileList(event.dataTransfer.files);
+      }
+
+      return { mouse: get_mouse_data(event), files };
+    }
+    case "click":
+    case "contextmenu":
+    case "doubleclick":
+    case "dblclick":
+    case "mousedown":
+    case "mouseenter":
+    case "mouseleave":
+    case "mousemove":
+    case "mouseout":
+    case "mouseover":
+    case "mouseup": {
+      return get_mouse_data(event);
+    }
+    case "pointerdown":
+    case "pointermove":
+    case "pointerup":
+    case "pointercancel":
+    case "gotpointercapture":
+    case "lostpointercapture":
+    case "pointerenter":
+    case "pointerleave":
+    case "pointerover":
+    case "pointerout": {
+      const {
+        altKey,
+        button,
+        buttons,
+        clientX,
+        clientY,
+        ctrlKey,
+        metaKey,
+        pageX,
+        pageY,
+        screenX,
+        screenY,
+        shiftKey,
+        pointerId,
+        width,
+        height,
+        pressure,
+        tangentialPressure,
+        tiltX,
+        tiltY,
+        twist,
+        pointerType,
+        isPrimary,
+      } = event;
+      return {
+        alt_key: altKey,
+        button: button,
+        buttons: buttons,
+        client_x: clientX,
+        client_y: clientY,
+        ctrl_key: ctrlKey,
+        meta_key: metaKey,
+        page_x: pageX,
+        page_y: pageY,
+        screen_x: screenX,
+        screen_y: screenY,
+        shift_key: shiftKey,
+        pointer_id: pointerId,
+        width: width,
+        height: height,
+        pressure: pressure,
+        tangential_pressure: tangentialPressure,
+        tilt_x: tiltX,
+        tilt_y: tiltY,
+        twist: twist,
+        pointer_type: pointerType,
+        is_primary: isPrimary,
+      };
+    }
+    case "select": {
+      return {};
+    }
+    case "touchcancel":
+    case "touchend":
+    case "touchmove":
+    case "touchstart": {
+      const { altKey, ctrlKey, metaKey, shiftKey } = event;
+      return {
+        // changed_touches: event.changedTouches,
+        // target_touches: event.targetTouches,
+        // touches: event.touches,
+        alt_key: altKey,
+        ctrl_key: ctrlKey,
+        meta_key: metaKey,
+        shift_key: shiftKey,
+      };
+    }
+    case "scroll": {
+      return {};
+    }
+    case "wheel": {
+      const { deltaX, deltaY, deltaZ, deltaMode } = event;
+      return {
+        delta_x: deltaX,
+        delta_y: deltaY,
+        delta_z: deltaZ,
+        delta_mode: deltaMode,
+      };
+    }
+    case "animationstart":
+    case "animationend":
+    case "animationiteration": {
+      const { animationName, elapsedTime, pseudoElement } = event;
+      return {
+        animation_name: animationName,
+        elapsed_time: elapsedTime,
+        pseudo_element: pseudoElement,
+      };
+    }
+    case "transitionend": {
+      const { propertyName, elapsedTime, pseudoElement } = event;
+      return {
+        property_name: propertyName,
+        elapsed_time: elapsedTime,
+        pseudo_element: pseudoElement,
+      };
+    }
+    case "abort":
+    case "canplay":
+    case "canplaythrough":
+    case "durationchange":
+    case "emptied":
+    case "encrypted":
+    case "ended":
+    case "error":
+    case "loadeddata":
+    case "loadedmetadata":
+    case "loadstart":
+    case "pause":
+    case "play":
+    case "playing":
+    case "progress":
+    case "ratechange":
+    case "seeked":
+    case "seeking":
+    case "stalled":
+    case "suspend":
+    case "timeupdate":
+    case "volumechange":
+    case "waiting": {
+      return {};
+    }
+    case "toggle": {
+      return {};
+    }
+    default: {
+      return {};
+    }
+  }
+}
+this.serializeIpcMessage = function (method, params = {}) {
+  return JSON.stringify({ method, params });
+}
+
+function is_element_node(node) {
+  return node.nodeType == 1;
+}
+
+function event_bubbles(event) {
+  switch (event) {
+    case "copy":
+      return true;
+    case "cut":
+      return true;
+    case "paste":
+      return true;
+    case "compositionend":
+      return true;
+    case "compositionstart":
+      return true;
+    case "compositionupdate":
+      return true;
+    case "keydown":
+      return true;
+    case "keypress":
+      return true;
+    case "keyup":
+      return true;
+    case "focus":
+      return false;
+    case "focusout":
+      return true;
+    case "focusin":
+      return true;
+    case "blur":
+      return false;
+    case "change":
+      return true;
+    case "input":
+      return true;
+    case "invalid":
+      return true;
+    case "reset":
+      return true;
+    case "submit":
+      return true;
+    case "click":
+      return true;
+    case "contextmenu":
+      return true;
+    case "doubleclick":
+      return true;
+    case "dblclick":
+      return true;
+    case "drag":
+      return true;
+    case "dragend":
+      return true;
+    case "dragenter":
+      return false;
+    case "dragexit":
+      return false;
+    case "dragleave":
+      return true;
+    case "dragover":
+      return true;
+    case "dragstart":
+      return true;
+    case "drop":
+      return true;
+    case "mousedown":
+      return true;
+    case "mouseenter":
+      return false;
+    case "mouseleave":
+      return false;
+    case "mousemove":
+      return true;
+    case "mouseout":
+      return true;
+    case "scroll":
+      return false;
+    case "mouseover":
+      return true;
+    case "mouseup":
+      return true;
+    case "pointerdown":
+      return true;
+    case "pointermove":
+      return true;
+    case "pointerup":
+      return true;
+    case "pointercancel":
+      return true;
+    case "gotpointercapture":
+      return true;
+    case "lostpointercapture":
+      return true;
+    case "pointerenter":
+      return false;
+    case "pointerleave":
+      return false;
+    case "pointerover":
+      return true;
+    case "pointerout":
+      return true;
+    case "select":
+      return true;
+    case "touchcancel":
+      return true;
+    case "touchend":
+      return true;
+    case "touchmove":
+      return true;
+    case "touchstart":
+      return true;
+    case "wheel":
+      return true;
+    case "abort":
+      return false;
+    case "canplay":
+      return false;
+    case "canplaythrough":
+      return false;
+    case "durationchange":
+      return false;
+    case "emptied":
+      return false;
+    case "encrypted":
+      return true;
+    case "ended":
+      return false;
+    case "error":
+      return false;
+    case "loadeddata":
+    case "loadedmetadata":
+    case "loadstart":
+    case "load":
+      return false;
+    case "pause":
+      return false;
+    case "play":
+      return false;
+    case "playing":
+      return false;
+    case "progress":
+      return false;
+    case "ratechange":
+      return false;
+    case "seeked":
+      return false;
+    case "seeking":
+      return false;
+    case "stalled":
+      return false;
+    case "suspend":
+      return false;
+    case "timeupdate":
+      return false;
+    case "volumechange":
+      return false;
+    case "waiting":
+      return false;
+    case "animationstart":
+      return true;
+    case "animationend":
+      return true;
+    case "animationiteration":
+      return true;
+    case "transitionend":
+      return true;
+    case "toggle":
+      return true;
+    case "mounted":
+      return false;
+  }
+
+  return true;
+}

+ 9 - 14
packages/interpreter/src/sledgehammer_bindings.rs

@@ -80,9 +80,7 @@ mod js {
     this.listeners = new ListenerMap();
     this.nodes = [];
     this.stack = [];
-    this.root = null;
     this.templates = {};
-    this.els = null;
     this.save_template = function(nodes, tmpl_id) {
         this.templates[tmpl_id] = nodes;
     }
@@ -131,15 +129,15 @@ mod js {
     }
     this.AppendChildren = function (id, many){
         let root = this.nodes[id];
-        this.els = this.stack.splice(this.stack.length-many);
+        let els = this.stack.splice(this.stack.length-many);
         for (let k = 0; k < many; k++) {
-            root.appendChild(this.els[k]);
+            root.appendChild(els[k]);
         }
     }
     "#;
 
     fn mount_to_root() {
-        "{this.AppendChildren(this.root, this.stack.length-1);}"
+        "{this.AppendChildren(this.listeners.root, this.stack.length-1);}"
     }
     fn push_root(root: u32) {
         "{this.stack.push(this.nodes[$root$]);}"
@@ -151,7 +149,7 @@ mod js {
         "{this.stack.pop();}"
     }
     fn replace_with(id: u32, n: u16) {
-        "{const root = this.nodes[$id$]; this.els = this.stack.splice(this.stack.length-$n$); if (root.listening) { this.listeners.removeAllNonBubbling(root); } root.replaceWith(...this.els);}"
+        "{const root = this.nodes[$id$]; let els = this.stack.splice(this.stack.length-$n$); if (root.listening) { this.listeners.removeAllNonBubbling(root); } root.replaceWith(...els);}"
     }
     fn insert_after(id: u32, n: u16) {
         "{this.nodes[$id$].after(...this.stack.splice(this.stack.length-$n$));}"
@@ -228,7 +226,7 @@ mod js {
         }"#
     }
     fn replace_placeholder(ptr: u32, len: u8, n: u16) {
-        "{this.els = this.stack.splice(this.stack.length - $n$); let node = this.LoadChild($ptr$, $len$); node.replaceWith(...this.els);}"
+        "{els = this.stack.splice(this.stack.length - $n$); let node = this.LoadChild($ptr$, $len$); node.replaceWith(...els);}"
     }
     fn load_template(tmpl_id: u16, index: u16, id: u32) {
         "{let node = this.templates[$tmpl_id$][$index$].cloneNode(true); this.nodes[$id$] = node; this.stack.push(node);}"
@@ -270,9 +268,6 @@ pub mod binary_protocol {
         const JS_FILE: &str = "./src/interpreter.js";
         const JS_FILE: &str = "./src/common.js";
 
-        fn mount_to_root() {
-            "{this.AppendChildren(this.root, this.stack.length-1);}"
-        }
         fn push_root(root: u32) {
             "{this.stack.push(this.nodes[$root$]);}"
         }
@@ -282,9 +277,9 @@ pub mod binary_protocol {
         fn append_children_to_top(many: u16) {
             "{
                 let root = this.stack[this.stack.length-many-1];
-                this.els = this.stack.splice(this.stack.length-many);
+                let els = this.stack.splice(this.stack.length-many);
                 for (let k = 0; k < many; k++) {
-                    root.appendChild(this.els[k]);
+                    root.appendChild(els[k]);
                 }
             }"
         }
@@ -292,7 +287,7 @@ pub mod binary_protocol {
             "{this.stack.pop();}"
         }
         fn replace_with(id: u32, n: u16) {
-            "{let root = this.nodes[$id$]; this.els = this.stack.splice(this.stack.length-$n$); if (root.listening) { this.listeners.removeAllNonBubbling(root); } root.replaceWith(...this.els);}"
+            "{let root = this.nodes[$id$]; let els = this.stack.splice(this.stack.length-$n$); if (root.listening) { this.listeners.removeAllNonBubbling(root); } root.replaceWith(...els);}"
         }
         fn insert_after(id: u32, n: u16) {
             "{this.nodes[$id$].after(...this.stack.splice(this.stack.length-$n$));}"
@@ -406,7 +401,7 @@ pub mod binary_protocol {
             }"#
         }
         fn replace_placeholder(array: &[u8], n: u16) {
-            "{this.els = this.stack.splice(this.stack.length - $n$); let node = this.LoadChild($array$); node.replaceWith(...this.els);}"
+            "{let els = this.stack.splice(this.stack.length - $n$); let node = this.LoadChild($array$); node.replaceWith(...els);}"
         }
         fn load_template(tmpl_id: u16, index: u16, id: u32) {
             "{let node = this.templates[$tmpl_id$][$index$].cloneNode(true); this.nodes[$id$] = node; this.stack.push(node);}"

+ 2 - 2
packages/router/src/hooks/use_navigator.rs

@@ -2,7 +2,7 @@ use dioxus_lib::prelude::{try_consume_context, use_hook};
 
 use crate::prelude::{Navigator, RouterContext};
 
-/// A hook that provides access to the navigator to change the router history. Unlike [`use_router`], this hook will not cause a rerender when the current route changes
+/// A hook that provides access to the navigator to change the router history.
 ///
 /// > The Routable macro will define a version of this hook with an explicit type.
 ///
@@ -26,7 +26,7 @@ use crate::prelude::{Navigator, RouterContext};
 ///
 /// #[component]
 /// fn Index() -> Element {
-///     let navigator = use_navigator(&cx);
+///     let navigator = use_navigator();
 ///
 ///     rsx! {
 ///         button {

+ 2 - 6
packages/router/src/hooks/use_route.rs

@@ -5,12 +5,8 @@ use crate::utils::use_router_internal::use_router_internal;
 ///
 /// > The Routable macro will define a version of this hook with an explicit type.
 ///
-/// # Return values
-/// - None, when not called inside a [`Link`] component.
-/// - Otherwise the current route.
-///
 /// # Panic
-/// - When the calling component is not nested within a [`Link`] component during a debug build.
+/// - When the calling component is not nested within a [`Router`] component.
 ///
 /// # Example
 /// ```rust
@@ -49,7 +45,7 @@ pub fn use_route<R: Routable + Clone>() -> R {
     match use_router_internal() {
         Some(r) => r.current(),
         None => {
-            panic!("`use_route` must have access to a parent router")
+            panic!("`use_route` must be called in a descendant of a Router component")
         }
     }
 }

+ 1 - 1
packages/router/src/router_cfg.rs

@@ -134,7 +134,7 @@ where
     return Box::new(AnyHistoryProviderImplWrapper::new(
         MemoryHistory::<R>::with_initial_path(
             dioxus_fullstack::prelude::server_context()
-                .request_parts()
+                .request_parts_blocking()
                 .uri
                 .to_string()
                 .parse()

+ 2 - 2
packages/router/src/utils/use_router_internal.rs

@@ -8,10 +8,10 @@ use crate::prelude::*;
 /// single component, but not recommended. Multiple subscriptions will be discarded.
 ///
 /// # Return values
-/// - [`None`], when the current component isn't a descendant of a [`Link`] component.
+/// - [`None`], when the current component isn't a descendant of a [`Router`] component.
 /// - Otherwise [`Some`].
 pub(crate) fn use_router_internal() -> Option<RouterContext> {
-    let router = use_hook(consume_context::<RouterContext>);
+    let router = try_consume_context::<RouterContext>()?;
     let id = current_scope_id().expect("use_router_internal called outside of a component");
     use_drop({
         to_owned![router];

+ 9 - 10
packages/signals/README.md

@@ -36,18 +36,18 @@ use dioxus_signals::*;
 #[component]
 fn App() -> Element {
     // Because signal is never read in this component, this component will not rerun when the signal changes
-    let signal = use_signal(|| 0);
+    let mut signal = use_signal(|| 0);
 
     rsx! {
         button {
             onclick: move |_| {
-                *signal.write() += 1;
+                signal += 1;
             },
             "Increase"
         }
         for id in 0..10 {
             Child {
-                signal: signal,
+                signal,
             }
         }
     }
@@ -58,11 +58,10 @@ struct ChildProps {
     signal: Signal<usize>,
 }
 
-#[component]
-fn Child(cx: Scope<ChildProps>) -> Element {
+fn Child(props: ChildProps) -> Element {
     // This component does read from the signal, so when the signal changes it will rerun
     rsx! {
-        "{cx.props.signal}"
+        "{props.signal}"
     }
 }
 ```
@@ -85,7 +84,7 @@ fn App() -> Element {
 
 #[component]
 fn Child() -> Element {
-    let signal: Signal<i32> = *use_context(cx).unwrap();
+    let signal: Signal<i32> = use_context();
     // This component does read from the signal, so when the signal changes it will rerun
     rsx! {
         "{signal}"
@@ -105,12 +104,12 @@ use dioxus_signals::*;
 
 #[component]
 fn App() -> Element {
-    let signal = use_signal(|| 0);
-    let doubled = use_memo(|| signal * 2);
+    let mut signal = use_signal(|| 0);
+    let doubled = use_memo(move || signal * 2);
 
     rsx! {
         button {
-            onclick: move |_| *signal.write() += 1,
+            onclick: move |_| signal += 1,
             "Increase"
         }
         Child {

+ 1 - 1
packages/signals/src/copy_value.rs

@@ -93,7 +93,7 @@ fn current_owner<S: Storage<T>, T>() -> Owner<S> {
 }
 
 fn owner_in_scope<S: Storage<T>, T>(scope: ScopeId) -> Owner<S> {
-    match consume_context_from_scope(scope) {
+    match scope.has_context() {
         Some(rt) => rt,
         None => {
             let owner = S::owner();

+ 47 - 2
packages/signals/src/reactive_context.rs

@@ -22,20 +22,47 @@ thread_local! {
     static CURRENT: RefCell<Vec<ReactiveContext>> = const { RefCell::new(vec![]) };
 }
 
+impl std::fmt::Display for ReactiveContext {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let read = self.inner.read();
+        match read.scope_subscriber {
+            Some(scope) => write!(f, "ReactiveContext for scope {:?}", scope),
+            None => {
+                #[cfg(debug_assertions)]
+                return write!(f, "ReactiveContext created at {}", read.origin);
+                #[cfg(not(debug_assertions))]
+                write!(f, "ReactiveContext")
+            }
+        }
+    }
+}
+
 impl Default for ReactiveContext {
+    #[track_caller]
     fn default() -> Self {
-        Self::new_for_scope(None)
+        Self::new_for_scope(None, std::panic::Location::caller())
     }
 }
 
 impl ReactiveContext {
     /// Create a new reactive context
+    #[track_caller]
     pub fn new() -> Self {
         Self::default()
     }
 
+    /// Create a new reactive context with a location for debugging purposes
+    /// This is useful for reactive contexts created within closures
+    pub fn new_with_origin(origin: &'static std::panic::Location<'static>) -> Self {
+        Self::new_for_scope(None, origin)
+    }
+
     /// Create a new reactive context that may update a scope
-    pub(crate) fn new_for_scope(scope: Option<ScopeId>) -> Self {
+    #[allow(unused)]
+    pub(crate) fn new_for_scope(
+        scope: Option<ScopeId>,
+        origin: &'static std::panic::Location<'static>,
+    ) -> Self {
         let (tx, rx) = flume::unbounded();
 
         let mut scope_subscribers = FxHashSet::default();
@@ -49,6 +76,8 @@ impl ReactiveContext {
             self_: None,
             update_any: schedule_update_any(),
             receiver: rx,
+            #[cfg(debug_assertions)]
+            origin,
         };
 
         let mut self_ = Self {
@@ -87,6 +116,7 @@ impl ReactiveContext {
         // Otherwise, create a new context at the current scope
         Some(provide_context(ReactiveContext::new_for_scope(
             current_scope_id(),
+            std::panic::Location::caller(),
         )))
     }
 
@@ -108,6 +138,17 @@ impl ReactiveContext {
     /// Returns true if the context was marked as dirty, or false if the context has been dropped
     pub fn mark_dirty(&self) -> bool {
         if let Ok(self_read) = self.inner.try_read() {
+            #[cfg(debug_assertions)]
+            {
+                if let Some(scope) = self_read.scope_subscriber {
+                    tracing::trace!("Marking reactive context for scope {:?} as dirty", scope);
+                } else {
+                    tracing::trace!(
+                        "Marking reactive context created at {} as dirty",
+                        self_read.origin
+                    );
+                }
+            }
             if let Some(scope) = self_read.scope_subscriber {
                 (self_read.update_any)(scope);
             }
@@ -148,4 +189,8 @@ struct Inner {
     // Futures will call .changed().await
     sender: flume::Sender<()>,
     receiver: flume::Receiver<()>,
+
+    // Debug information for signal subscriptions
+    #[cfg(debug_assertions)]
+    origin: &'static std::panic::Location<'static>,
 }

+ 3 - 1
packages/signals/src/read.rs

@@ -39,7 +39,9 @@ pub trait Readable {
         MappedSignal::new(try_read, peek)
     }
 
-    /// Get the current value of the state. If this is a signal, this will subscribe the current scope to the signal. If the value has been dropped, this will panic.
+    /// Get the current value of the state. If this is a signal, this will subscribe the current scope to the signal.
+    /// If the value has been dropped, this will panic. Calling this on a Signal is the same as
+    /// using the signal() syntax to read and subscribe to its value
     #[track_caller]
     fn read(&self) -> ReadableRef<Self> {
         self.try_read().unwrap()

+ 17 - 7
packages/signals/src/signal.rs

@@ -30,17 +30,15 @@ use std::{
 /// }
 ///
 /// #[component]
-/// fn Child(state: Signal<u32>) -> Element {
-///     let state = *state;
-///
-///     use_future( |()| async move {
+/// fn Child(mut state: Signal<u32>) -> Element {
+///     use_future(move || async move {
 ///         // Because the signal is a Copy type, we can use it in an async block without cloning it.
-///         *state.write() += 1;
+///         state += 1;
 ///     });
 ///
 ///     rsx! {
 ///         button {
-///             onclick: move |_| *state.write() += 1,
+///             onclick: move |_| state += 1,
 ///             "{state}"
 ///         }
 ///     }
@@ -202,6 +200,7 @@ impl<T, S: Storage<SignalData<T>>> Readable for Signal<T, S> {
         let inner = self.inner.try_read()?;
 
         if let Some(reactive_context) = ReactiveContext::current() {
+            tracing::trace!("Subscribing to the reactive context {}", reactive_context);
             inner.subscribers.lock().unwrap().insert(reactive_context);
         }
 
@@ -244,7 +243,11 @@ impl<T: 'static, S: Storage<SignalData<T>>> Writable for Signal<T, S> {
             let borrow = S::map_mut(inner, |v| &mut v.value);
             Write {
                 write: borrow,
-                drop_signal: Box::new(SignalSubscriberDrop { signal: *self }),
+                drop_signal: Box::new(SignalSubscriberDrop {
+                    signal: *self,
+                    #[cfg(debug_assertions)]
+                    origin: std::panic::Location::caller(),
+                }),
             }
         })
     }
@@ -344,10 +347,17 @@ impl<T: ?Sized, S: AnyStorage> DerefMut for Write<T, S> {
 
 struct SignalSubscriberDrop<T: 'static, S: Storage<SignalData<T>>> {
     signal: Signal<T, S>,
+    #[cfg(debug_assertions)]
+    origin: &'static std::panic::Location<'static>,
 }
 
 impl<T: 'static, S: Storage<SignalData<T>>> Drop for SignalSubscriberDrop<T, S> {
     fn drop(&mut self) {
+        #[cfg(debug_assertions)]
+        tracing::trace!(
+            "Write on signal at {:?} finished, updating subscribers",
+            self.origin
+        );
         self.signal.update_subscribers();
     }
 }

+ 16 - 5
packages/signals/tests/create.rs

@@ -56,6 +56,11 @@ fn deref_signal() {
 
 #[test]
 fn drop_signals() {
+    use std::sync::atomic::AtomicUsize;
+    use std::sync::atomic::Ordering;
+
+    static SIGNAL_DROP_COUNT: AtomicUsize = AtomicUsize::new(0);
+
     let mut dom = VirtualDom::new(|| {
         let generation = generation();
 
@@ -68,10 +73,18 @@ fn drop_signals() {
     });
 
     fn Child() -> Element {
-        let signal = create_without_cx();
+        struct TracksDrops;
+
+        impl Drop for TracksDrops {
+            fn drop(&mut self) {
+                SIGNAL_DROP_COUNT.fetch_add(1, Ordering::Relaxed);
+            }
+        }
+
+        use_signal(|| TracksDrops);
 
         rsx! {
-            "{signal}"
+            ""
         }
     }
 
@@ -79,7 +92,5 @@ fn drop_signals() {
     dom.mark_dirty(ScopeId::ROOT);
     dom.render_immediate(&mut NoOpMutations);
 
-    fn create_without_cx() -> Signal<String> {
-        Signal::new("hello world".to_string())
-    }
+    assert_eq!(SIGNAL_DROP_COUNT.load(Ordering::Relaxed), 10);
 }