diff --git a/Cargo.lock b/Cargo.lock index bc1064e8c..f037f0a10 100644 --- a/Cargo.lock +++ b/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" diff --git a/examples/future.rs b/examples/future.rs index 130d526b2..76b6c8e7f 100644 --- a/examples/future.rs +++ b/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; diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index a4626b65a..a469eb04c 100644 --- a/packages/core-macro/src/lib.rs +++ b/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}" } /// } diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 01ed1847d..765f9b12f 100644 --- a/packages/core/Cargo.toml +++ b/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" diff --git a/packages/core/src/diff/component.rs b/packages/core/src/diff/component.rs index 6d4ab005e..651823c5c 100644 --- a/packages/core/src/diff/component.rs +++ b/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; } diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 632b42fa3..03c438978 100644 --- a/packages/core/src/scopes.rs +++ b/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. /// diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 2de7f0514..b1a4e1203 100644 --- a/packages/core/src/virtual_dom.rs +++ b/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(&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, @@ -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) { let el_ref = &self.mounts[node.mount.0].node; let node_template = el_ref.template.get(); diff --git a/packages/core/tests/tracing.rs b/packages/core/tests/tracing.rs new file mode 100644 index 000000000..1fe04033d --- /dev/null +++ b/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::::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) -> Element { + rsx! { li { onmouseover: move |e| onhover.call(e), "{i}" } } +} diff --git a/packages/fullstack/src/render.rs b/packages/fullstack/src/render.rs index ea179207f..42b6395ee 100644 --- a/packages/fullstack/src/render.rs +++ b/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 diff --git a/packages/fullstack/src/server_context.rs b/packages/fullstack/src/server_context.rs index a2cbfeb55..7ddc67f3b 100644 --- a/packages/fullstack/src/server_context.rs +++ b/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 { - Ok(I::from_request_parts(&mut req.request_parts_mut(), &()).await?) + Ok(I::from_request_parts(&mut *req.request_parts_mut().await, &()).await?) } } diff --git a/packages/hooks/src/use_context.rs b/packages/hooks/src/use_context.rs index ac3bb62ea..9194dbd2f 100644 --- a/packages/hooks/src/use_context.rs +++ b/packages/hooks/src/use_context.rs @@ -14,6 +14,18 @@ pub fn try_use_context() -> Option { /// 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::(); +/// rsx! { "user using dark mode: {user_theme == Theme::Dark}" } +/// } +/// ``` #[must_use] pub fn use_context() -> T { use_hook(|| consume_context::()) @@ -22,6 +34,24 @@ pub fn use_context() -> 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 = use_context(); +/// rsx! { +/// button { onclick: move |_| signal += 1, "increment context" } +/// p {"{signal}"} +/// } +///} +/// ``` pub fn use_context_provider(f: impl FnOnce() -> T) -> T { use_hook(|| { let val = f(); diff --git a/packages/hooks/src/use_coroutine.rs b/packages/hooks/src/use_coroutine.rs index 06f342ceb..4834c5431 100644 --- a/packages/hooks/src/use_coroutine.rs +++ b/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() -> Coroutine { diff --git a/packages/hooks/src/use_effect.rs b/packages/hooks/src/use_effect.rs index a3d16f420..9124c1dcb 100644 --- a/packages/hooks/src/use_effect.rs +++ b/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); diff --git a/packages/hooks/src/use_future.rs b/packages/hooks/src/use_future.rs index 4d69879aa..53a67087c 100644 --- a/packages/hooks/src/use_future.rs +++ b/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(mut future: impl FnMut() -> F + 'static) -> UseFuture where F: Future + 'static, diff --git a/packages/hooks/src/use_resource.rs b/packages/hooks/src/use_resource.rs index 222b62e7e..389697d74 100644 --- a/packages/hooks/src/use_resource.rs +++ b/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(future: impl Fn() -> F + 'static) -> Resource where diff --git a/packages/html/src/elements.rs b/packages/html/src/elements.rs index c5e5e3ab5..5275fa4f6 100644 --- a/packages/html/src/elements.rs +++ b/packages/html/src/elements.rs @@ -1613,10 +1613,10 @@ builder_constructors! { /// element. hatchpath "http://www.w3.org/2000/svg" {}; - // /// Build a - // /// [``](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/image) - // /// element. - // image "http://www.w3.org/2000/svg" {}; + /// Build a + /// [``](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/image) + /// element. + image "http://www.w3.org/2000/svg" {}; /// Build a /// [``](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/line) diff --git a/packages/interpreter/src/interpreter.js b/packages/interpreter/src/interpreter.js index c3d63316e..63ee6d630 100644 --- a/packages/interpreter/src/interpreter.js +++ b/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; +} diff --git a/packages/interpreter/src/sledgehammer_bindings.rs b/packages/interpreter/src/sledgehammer_bindings.rs index 18e4b9df5..1fbdc9823 100644 --- a/packages/interpreter/src/sledgehammer_bindings.rs +++ b/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);}" diff --git a/packages/router/src/hooks/use_navigator.rs b/packages/router/src/hooks/use_navigator.rs index 57a3bd3b8..1207b1fb2 100644 --- a/packages/router/src/hooks/use_navigator.rs +++ b/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 { diff --git a/packages/router/src/hooks/use_route.rs b/packages/router/src/hooks/use_route.rs index 7a9942bd1..d0c66ebfb 100644 --- a/packages/router/src/hooks/use_route.rs +++ b/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 { 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") } } } diff --git a/packages/router/src/router_cfg.rs b/packages/router/src/router_cfg.rs index 8ec762b14..f4e9177d3 100644 --- a/packages/router/src/router_cfg.rs +++ b/packages/router/src/router_cfg.rs @@ -134,7 +134,7 @@ where return Box::new(AnyHistoryProviderImplWrapper::new( MemoryHistory::::with_initial_path( dioxus_fullstack::prelude::server_context() - .request_parts() + .request_parts_blocking() .uri .to_string() .parse() diff --git a/packages/router/src/utils/use_router_internal.rs b/packages/router/src/utils/use_router_internal.rs index 441063cbe..04853233c 100644 --- a/packages/router/src/utils/use_router_internal.rs +++ b/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 { - let router = use_hook(consume_context::); + let router = try_consume_context::()?; let id = current_scope_id().expect("use_router_internal called outside of a component"); use_drop({ to_owned![router]; diff --git a/packages/signals/README.md b/packages/signals/README.md index 56dd8d47d..7a1f39f0d 100644 --- a/packages/signals/README.md +++ b/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, } -#[component] -fn Child(cx: Scope) -> 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 = *use_context(cx).unwrap(); + let signal: Signal = 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 { diff --git a/packages/signals/src/copy_value.rs b/packages/signals/src/copy_value.rs index 6efe3c70f..ca318bf2c 100644 --- a/packages/signals/src/copy_value.rs +++ b/packages/signals/src/copy_value.rs @@ -93,7 +93,7 @@ fn current_owner, T>() -> Owner { } fn owner_in_scope, T>(scope: ScopeId) -> Owner { - match consume_context_from_scope(scope) { + match scope.has_context() { Some(rt) => rt, None => { let owner = S::owner(); diff --git a/packages/signals/src/reactive_context.rs b/packages/signals/src/reactive_context.rs index fdbaf6429..98b611bb1 100644 --- a/packages/signals/src/reactive_context.rs +++ b/packages/signals/src/reactive_context.rs @@ -22,20 +22,47 @@ thread_local! { static CURRENT: RefCell> = 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) -> Self { + #[allow(unused)] + pub(crate) fn new_for_scope( + scope: Option, + 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>, } diff --git a/packages/signals/src/read.rs b/packages/signals/src/read.rs index 63de45047..7effa39f7 100644 --- a/packages/signals/src/read.rs +++ b/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.try_read().unwrap() diff --git a/packages/signals/src/signal.rs b/packages/signals/src/signal.rs index 44de51082..b0a70d210 100644 --- a/packages/signals/src/signal.rs +++ b/packages/signals/src/signal.rs @@ -30,17 +30,15 @@ use std::{ /// } /// /// #[component] -/// fn Child(state: Signal) -> Element { -/// let state = *state; -/// -/// use_future( |()| async move { +/// fn Child(mut state: Signal) -> 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>> Readable for Signal { 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>> Writable for Signal { 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 DerefMut for Write { struct SignalSubscriberDrop>> { signal: Signal, + #[cfg(debug_assertions)] + origin: &'static std::panic::Location<'static>, } impl>> Drop for SignalSubscriberDrop { fn drop(&mut self) { + #[cfg(debug_assertions)] + tracing::trace!( + "Write on signal at {:?} finished, updating subscribers", + self.origin + ); self.signal.update_subscribers(); } } diff --git a/packages/signals/tests/create.rs b/packages/signals/tests/create.rs index 7b17310b6..b32da529c 100644 --- a/packages/signals/tests/create.rs +++ b/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 { - Signal::new("hello world".to_string()) - } + assert_eq!(SIGNAL_DROP_COUNT.load(Ordering::Relaxed), 10); }