diff --git a/packages/yew-router/tests/basename.rs b/packages/yew-router/tests/basename.rs index bba06828f2d..97d65320eac 100644 --- a/packages/yew-router/tests/basename.rs +++ b/packages/yew-router/tests/basename.rs @@ -1,4 +1,6 @@ +use gloo::timers::future::sleep; use serde::{Deserialize, Serialize}; +use std::time::Duration; use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; use yew::functional::function_component; use yew::prelude::*; @@ -112,19 +114,29 @@ fn root() -> Html { // - query parameters // - 404 redirects #[test] -fn router_works() { +async fn router_works() { yew::start_app_in_element::(gloo::utils::document().get_element_by_id("output").unwrap()); + sleep(Duration::ZERO).await; + assert_eq!("Home", obtain_result_by_id("result")); + sleep(Duration::ZERO).await; + let initial_length = history_length(); + sleep(Duration::ZERO).await; + click("button"); // replacing the current route + + sleep(Duration::ZERO).await; assert_eq!("2", obtain_result_by_id("result-params")); assert_eq!("bar", obtain_result_by_id("result-query")); assert_eq!(initial_length, history_length()); click("button"); // pushing a new route + + sleep(Duration::ZERO).await; assert_eq!("3", obtain_result_by_id("result-params")); assert_eq!("baz", obtain_result_by_id("result-query")); assert_eq!(initial_length + 1, history_length()); diff --git a/packages/yew-router/tests/browser_router.rs b/packages/yew-router/tests/browser_router.rs index 144a31d8e1c..d720e2546ca 100644 --- a/packages/yew-router/tests/browser_router.rs +++ b/packages/yew-router/tests/browser_router.rs @@ -1,4 +1,6 @@ +use gloo::timers::future::sleep; use serde::{Deserialize, Serialize}; +use std::time::Duration; use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; use yew::functional::function_component; use yew::prelude::*; @@ -112,19 +114,29 @@ fn root() -> Html { // - query parameters // - 404 redirects #[test] -fn router_works() { +async fn router_works() { yew::start_app_in_element::(gloo::utils::document().get_element_by_id("output").unwrap()); + sleep(Duration::ZERO).await; + assert_eq!("Home", obtain_result_by_id("result")); + sleep(Duration::ZERO).await; + let initial_length = history_length(); + sleep(Duration::ZERO).await; + click("button"); // replacing the current route + + sleep(Duration::ZERO).await; assert_eq!("2", obtain_result_by_id("result-params")); assert_eq!("bar", obtain_result_by_id("result-query")); assert_eq!(initial_length, history_length()); click("button"); // pushing a new route + + sleep(Duration::ZERO).await; assert_eq!("3", obtain_result_by_id("result-params")); assert_eq!("baz", obtain_result_by_id("result-query")); assert_eq!(initial_length + 1, history_length()); diff --git a/packages/yew-router/tests/hash_router.rs b/packages/yew-router/tests/hash_router.rs index d6b691f2bc2..1e75396cfdd 100644 --- a/packages/yew-router/tests/hash_router.rs +++ b/packages/yew-router/tests/hash_router.rs @@ -1,4 +1,6 @@ +use gloo::timers::future::sleep; use serde::{Deserialize, Serialize}; +use std::time::Duration; use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; use yew::functional::function_component; use yew::prelude::*; @@ -112,19 +114,29 @@ fn root() -> Html { // - query parameters // - 404 redirects #[test] -fn router_works() { +async fn router_works() { yew::start_app_in_element::(gloo::utils::document().get_element_by_id("output").unwrap()); + sleep(Duration::ZERO).await; + assert_eq!("Home", obtain_result_by_id("result")); + sleep(Duration::ZERO).await; + let initial_length = history_length(); + sleep(Duration::ZERO).await; + click("button"); // replacing the current route + + sleep(Duration::ZERO).await; assert_eq!("2", obtain_result_by_id("result-params")); assert_eq!("bar", obtain_result_by_id("result-query")); assert_eq!(initial_length, history_length()); click("button"); // pushing a new route + + sleep(Duration::ZERO).await; assert_eq!("3", obtain_result_by_id("result-params")); assert_eq!("baz", obtain_result_by_id("result-query")); assert_eq!(initial_length + 1, history_length()); diff --git a/packages/yew/src/html/component/lifecycle.rs b/packages/yew/src/html/component/lifecycle.rs index 45c03576811..15595b91de0 100644 --- a/packages/yew/src/html/component/lifecycle.rs +++ b/packages/yew/src/html/component/lifecycle.rs @@ -1,7 +1,7 @@ //! Component lifecycle module use super::{AnyScope, BaseComponent, Scope}; -use crate::html::RenderError; +use crate::html::{RenderError, RenderResult}; use crate::scheduler::{self, Runnable, Shared}; use crate::suspense::{Suspense, Suspension}; use crate::virtual_dom::{VDiff, VNode}; @@ -9,14 +9,94 @@ use crate::Callback; use crate::{Context, NodeRef}; #[cfg(feature = "ssr")] use futures::channel::oneshot; +use std::any::Any; use std::rc::Rc; use web_sys::Element; -pub(crate) struct ComponentState { - pub(crate) component: Box, - pub(crate) root_node: VNode, +pub(crate) struct CompStateInner +where + COMP: BaseComponent, +{ + pub(crate) component: COMP, + pub(crate) context: Context, +} + +/// A trait to provide common, +/// generic free behaviour across all components to reduce code size. +/// +/// Mostly a thin wrapper that passes the context to a component's lifecycle +/// methods. +pub(crate) trait Stateful { + fn view(&self) -> RenderResult; + fn rendered(&mut self, first_render: bool); + fn destroy(&mut self); + + fn any_scope(&self) -> AnyScope; + + fn flush_messages(&mut self) -> bool; + fn props_changed(&mut self, props: Rc) -> bool; + + fn as_any(&self) -> &dyn Any; + fn as_any_mut(&mut self) -> &mut dyn Any; +} + +impl Stateful for CompStateInner +where + COMP: BaseComponent, +{ + fn view(&self) -> RenderResult { + self.component.view(&self.context) + } + + fn rendered(&mut self, first_render: bool) { + self.component.rendered(&self.context, first_render) + } + + fn destroy(&mut self) { + self.component.destroy(&self.context); + } + + fn any_scope(&self) -> AnyScope { + self.context.link().clone().into() + } + + fn flush_messages(&mut self) -> bool { + self.context + .link() + .pending_messages + .drain() + .into_iter() + .fold(false, |acc, msg| { + self.component.update(&self.context, msg) || acc + }) + } + + fn props_changed(&mut self, props: Rc) -> bool { + let props = match Rc::downcast::(props) { + Ok(m) => m, + _ => return false, + }; - context: Context, + if self.context.props != props { + self.context.props = Rc::clone(&props); + self.component.changed(&self.context) + } else { + false + } + } + + fn as_any(&self) -> &dyn Any { + self + } + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } +} + +pub(crate) struct ComponentState { + pub(crate) inner: Box, + + pub(crate) root_node: VNode, /// When a component has no parent, it means that it should not be rendered. parent: Option, @@ -32,11 +112,11 @@ pub(crate) struct ComponentState { // Used for debug logging #[cfg(debug_assertions)] - pub(crate) vcomp_id: u64, + pub(crate) vcomp_id: usize, } -impl ComponentState { - pub(crate) fn new( +impl ComponentState { + pub(crate) fn new( parent: Option, next_sibling: NodeRef, root_node: VNode, @@ -46,18 +126,17 @@ impl ComponentState { #[cfg(feature = "ssr")] html_sender: Option>, ) -> Self { #[cfg(debug_assertions)] - let vcomp_id = { - use super::Scoped; - - scope.to_any().vcomp_id - }; + let vcomp_id = scope.vcomp_id; let context = Context { scope, props }; - let component = Box::new(COMP::create(&context)); + let inner = Box::new(CompStateInner { + component: COMP::create(&context), + context, + }); + Self { - component, + inner, root_node, - context, parent, next_sibling, node_ref, @@ -105,44 +184,33 @@ impl Runnable for CreateRunner { } } -pub(crate) enum UpdateEvent { - /// Wraps messages for a component. - Message(COMP::Message), - /// Wraps batch of messages for a component. - MessageBatch(Vec), +pub(crate) enum UpdateEvent { + /// Drain messages for a component. + Message, /// Wraps properties, node ref, and next sibling for a component. - Properties(Rc, NodeRef, NodeRef), + Properties(Rc, NodeRef, NodeRef), /// Shift Scope. Shift(Element, NodeRef), } -pub(crate) struct UpdateRunner { - pub(crate) state: Shared>>, - pub(crate) event: UpdateEvent, +pub(crate) struct UpdateRunner { + pub(crate) state: Shared>, + pub(crate) event: UpdateEvent, } -impl Runnable for UpdateRunner { +impl Runnable for UpdateRunner { fn run(self: Box) { if let Some(mut state) = self.state.borrow_mut().as_mut() { let schedule_render = match self.event { - UpdateEvent::Message(message) => state.component.update(&state.context, message), - UpdateEvent::MessageBatch(messages) => { - messages.into_iter().fold(false, |acc, msg| { - state.component.update(&state.context, msg) || acc - }) - } + UpdateEvent::Message => state.inner.flush_messages(), UpdateEvent::Properties(props, node_ref, next_sibling) => { // When components are updated, a new node ref could have been passed in state.node_ref = node_ref; // When components are updated, their siblings were likely also updated state.next_sibling = next_sibling; // Only trigger changed if props were changed - if state.context.props != props { - state.context.props = Rc::clone(&props); - state.component.changed(&state.context) - } else { - false - } + + state.inner.props_changed(props) } UpdateEvent::Shift(parent, next_sibling) => { state.root_node.shift( @@ -177,18 +245,18 @@ impl Runnable for UpdateRunner { } } -pub(crate) struct DestroyRunner { - pub(crate) state: Shared>>, +pub(crate) struct DestroyRunner { + pub(crate) state: Shared>, pub(crate) parent_to_detach: bool, } -impl Runnable for DestroyRunner { +impl Runnable for DestroyRunner { fn run(self: Box) { if let Some(mut state) = self.state.borrow_mut().take() { #[cfg(debug_assertions)] crate::virtual_dom::vcomp::log_event(state.vcomp_id, "destroy"); - state.component.destroy(&state.context); + state.inner.destroy(); if let Some(ref m) = state.parent { state.root_node.detach(m, self.parent_to_detach); @@ -198,17 +266,17 @@ impl Runnable for DestroyRunner { } } -pub(crate) struct RenderRunner { - pub(crate) state: Shared>>, +pub(crate) struct RenderRunner { + pub(crate) state: Shared>, } -impl Runnable for RenderRunner { +impl Runnable for RenderRunner { fn run(self: Box) { if let Some(state) = self.state.borrow_mut().as_mut() { #[cfg(debug_assertions)] crate::virtual_dom::vcomp::log_event(state.vcomp_id, "render"); - match state.component.view(&state.context) { + match state.inner.view() { Ok(m) => { // Currently not suspended, we remove any previous suspension and update // normally. @@ -218,7 +286,7 @@ impl Runnable for RenderRunner { } if let Some(m) = state.suspension.take() { - let comp_scope = AnyScope::from(state.context.scope.clone()); + let comp_scope = state.inner.any_scope(); let suspense_scope = comp_scope.find_parent_scope::().unwrap(); let suspense = suspense_scope.get_component().unwrap(); @@ -229,7 +297,7 @@ impl Runnable for RenderRunner { if let Some(ref m) = state.parent { let ancestor = Some(root); let new_root = &mut state.root_node; - let scope = state.context.scope.clone().into(); + let scope = state.inner.any_scope(); let next_sibling = state.next_sibling.clone(); let node = new_root.apply(&scope, m, next_sibling, ancestor); @@ -271,7 +339,7 @@ impl Runnable for RenderRunner { } else { // We schedule a render after current suspension is resumed. - let comp_scope = AnyScope::from(state.context.scope.clone()); + let comp_scope = state.inner.any_scope(); let suspense_scope = comp_scope .find_parent_scope::() @@ -285,6 +353,7 @@ impl Runnable for RenderRunner { state: shared_state.clone(), }, ); + scheduler::start(); })); if let Some(ref last_m) = state.suspension { @@ -303,19 +372,19 @@ impl Runnable for RenderRunner { } } -pub(crate) struct RenderedRunner { - pub(crate) state: Shared>>, +pub(crate) struct RenderedRunner { + pub(crate) state: Shared>, first_render: bool, } -impl Runnable for RenderedRunner { +impl Runnable for RenderedRunner { fn run(self: Box) { if let Some(state) = self.state.borrow_mut().as_mut() { #[cfg(debug_assertions)] crate::virtual_dom::vcomp::log_event(state.vcomp_id, "rendered"); if state.suspension.is_none() && state.parent.is_some() { - state.component.rendered(&state.context, self.first_render); + state.inner.rendered(self.first_render); } } } @@ -450,6 +519,7 @@ mod tests { lifecycle.borrow_mut().clear(); scope.mount_in_place(el, NodeRef::default(), NodeRef::default(), Rc::new(props)); + crate::scheduler::start_now(); assert_eq!(&lifecycle.borrow_mut().deref()[..], expected); } diff --git a/packages/yew/src/html/component/scope.rs b/packages/yew/src/html/component/scope.rs index 0a915545195..d93f48586d6 100644 --- a/packages/yew/src/html/component/scope.rs +++ b/packages/yew/src/html/component/scope.rs @@ -2,7 +2,8 @@ use super::{ lifecycle::{ - ComponentState, CreateRunner, DestroyRunner, RenderRunner, UpdateEvent, UpdateRunner, + CompStateInner, ComponentState, CreateRunner, DestroyRunner, RenderRunner, UpdateEvent, + UpdateRunner, }, BaseComponent, }; @@ -12,23 +13,67 @@ use crate::html::NodeRef; use crate::scheduler::{self, Shared}; use crate::virtual_dom::{insert_node, VNode}; use gloo_utils::document; -use std::any::{Any, TypeId}; +use std::any::TypeId; use std::cell::{Ref, RefCell}; +use std::marker::PhantomData; use std::ops::Deref; use std::rc::Rc; use std::{fmt, iter}; use web_sys::{Element, Node}; +#[derive(Debug)] +pub(crate) struct MsgQueue(Shared>); + +impl MsgQueue { + pub fn new() -> Self { + MsgQueue(Rc::default()) + } + + pub fn push(&self, msg: Msg) -> usize { + let mut inner = self.0.borrow_mut(); + inner.push(msg); + + inner.len() + } + + pub fn append(&self, other: &mut Vec) -> usize { + let mut inner = self.0.borrow_mut(); + inner.append(other); + + inner.len() + } + + pub fn drain(&self) -> Vec { + let mut other_queue = Vec::new(); + let mut inner = self.0.borrow_mut(); + + std::mem::swap(&mut *inner, &mut other_queue); + + other_queue + } +} + +impl Clone for MsgQueue { + fn clone(&self) -> Self { + MsgQueue(self.0.clone()) + } +} + /// Untyped scope used for accessing parent scope -#[derive(Debug, Clone)] +#[derive(Clone)] pub struct AnyScope { type_id: TypeId, parent: Option>, - state: Rc, + state: Shared>, - // Used for debug logging #[cfg(debug_assertions)] - pub(crate) vcomp_id: u64, + pub(crate) vcomp_id: usize, +} + +impl fmt::Debug for AnyScope { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("AnyScope<_>") + } } impl From> for AnyScope { @@ -50,7 +95,7 @@ impl AnyScope { Self { type_id: TypeId::of::<()>(), parent: None, - state: Rc::new(()), + state: Rc::new(RefCell::new(None)), #[cfg(debug_assertions)] vcomp_id: 0, @@ -69,25 +114,20 @@ impl AnyScope { /// Attempts to downcast into a typed scope pub fn downcast(self) -> Scope { - let state = self - .state - .downcast::>>>() - .expect("unexpected component type"); + let state = self.state.borrow(); - #[cfg(debug_assertions)] - let vcomp_id = state - .borrow() + state .as_ref() - .map(|s| s.vcomp_id) - .unwrap_or_default(); - - Scope { - parent: self.parent, - state, - - #[cfg(debug_assertions)] - vcomp_id, - } + .map(|m| { + m.inner + .as_any() + .downcast_ref::>() + .unwrap() + .context + .link() + .clone() + }) + .unwrap() } pub(crate) fn find_parent_scope(&self) -> Option> { @@ -155,12 +195,13 @@ impl Scoped for Scope { /// A context which allows sending messages to a component. pub struct Scope { + _marker: PhantomData, parent: Option>, - pub(crate) state: Shared>>, + pub(crate) pending_messages: MsgQueue, + pub(crate) state: Shared>, - // Used for debug logging #[cfg(debug_assertions)] - pub(crate) vcomp_id: u64, + pub(crate) vcomp_id: usize, } impl fmt::Debug for Scope { @@ -172,6 +213,8 @@ impl fmt::Debug for Scope { impl Clone for Scope { fn clone(&self) -> Self { Scope { + _marker: PhantomData, + pending_messages: self.pending_messages.clone(), parent: self.parent.clone(), state: self.state.clone(), @@ -192,7 +235,14 @@ impl Scope { self.state.try_borrow().ok().and_then(|state_ref| { state_ref.as_ref()?; Some(Ref::map(state_ref, |state| { - state.as_ref().unwrap().component.as_ref() + &state + .as_ref() + .unwrap() + .inner + .as_any() + .downcast_ref::>() + .unwrap() + .component })) }) } @@ -200,11 +250,14 @@ impl Scope { pub(crate) fn new(parent: Option) -> Self { let parent = parent.map(Rc::new); let state = Rc::new(RefCell::new(None)); + let pending_messages = MsgQueue::new(); #[cfg(debug_assertions)] let vcomp_id = parent.as_ref().map(|p| p.vcomp_id).unwrap_or_default(); Scope { + _marker: PhantomData, + pending_messages, state, parent, @@ -261,7 +314,7 @@ impl Scope { self.push_update(UpdateEvent::Properties(props, node_ref, next_sibling)); } - fn push_update(&self, event: UpdateEvent) { + fn push_update(&self, event: UpdateEvent) { scheduler::push_component_update(UpdateRunner { state: self.state.clone(), event, @@ -271,40 +324,28 @@ impl Scope { } /// Send a message to the component. - /// - /// Please be aware that currently this method synchronously - /// schedules a call to the [Component](crate::html::Component) interface. pub fn send_message(&self, msg: T) where T: Into, { - self.push_update(UpdateEvent::Message(msg.into())); + // We are the first message in queue, so we queue the update. + if self.pending_messages.push(msg.into()) == 1 { + self.push_update(UpdateEvent::Message); + } } /// Send a batch of messages to the component. - /// - /// This is useful for reducing re-renders of the components - /// because the messages are handled together and the view - /// function is called only once if needed. - /// - /// Please be aware that currently this method synchronously - /// schedules calls to the [Component](crate::html::Component) interface. - pub fn send_message_batch(&self, messages: Vec) { - // There is no reason to schedule empty batches. - // This check is especially handy for the batch_callback method. - if messages.is_empty() { - return; - } + pub fn send_message_batch(&self, mut messages: Vec) { + let msg_len = messages.len(); - self.push_update(UpdateEvent::MessageBatch(messages)); + // The queue was empty, so we queue the update + if self.pending_messages.append(&mut messages) == msg_len { + self.push_update(UpdateEvent::Message); + } } /// Creates a `Callback` which will send a message to the linked /// component's update method when invoked. - /// - /// Please be aware that currently the result of this callback - /// synchronously schedules a call to the [Component](crate::Component) - /// interface. pub fn callback(&self, function: F) -> Callback where M: Into, @@ -329,10 +370,6 @@ impl Scope { /// link.batch_callback(|_| vec![Msg::A, Msg::B]); /// link.batch_callback(|_| Some(Msg::A)); /// ``` - /// - /// Please be aware that currently the results of these callbacks - /// will synchronously schedule calls to the - /// [Component](crate::Component) interface. pub fn batch_callback(&self, function: F) -> Callback where F: Fn(IN) -> OUT + 'static, diff --git a/packages/yew/src/scheduler.rs b/packages/yew/src/scheduler.rs index 0ee179778fc..ef888d71d44 100644 --- a/packages/yew/src/scheduler.rs +++ b/packages/yew/src/scheduler.rs @@ -99,7 +99,7 @@ pub(crate) fn push_component_update(runnable: impl Runnable + 'static) { } /// Execute any pending [Runnable]s -pub(crate) fn start() { +pub(crate) fn start_now() { thread_local! { // The lock is used to prevent recursion. If the lock cannot be acquired, it is because the // `start()` method is being called recursively as part of a `runnable.run()`. @@ -122,6 +122,40 @@ pub(crate) fn start() { }); } +#[cfg(target_arch = "wasm32")] +mod target_wasm { + use super::*; + use crate::io_coop::spawn_local; + + /// We delay the start of the scheduler to the end of the micro task queue. + /// So any messages that needs to be queued can be queued. + pub(crate) fn start() { + spawn_local(async { + start_now(); + }); + } +} + +#[cfg(target_arch = "wasm32")] +pub(crate) use target_wasm::*; + +#[cfg(not(target_arch = "wasm32"))] +mod target_native { + use super::*; + + // Delayed rendering is not very useful in the context of server-side rendering. + // There are no event listeners or other high priority events that need to be + // processed and we risk of having a future un-finished. + // Until scheduler is future-capable which means we can join inside a future, + // it can remain synchronous. + pub(crate) fn start() { + start_now(); + } +} + +#[cfg(not(target_arch = "wasm32"))] +pub(crate) use target_native::*; + impl Scheduler { /// Fill vector with tasks to be executed according to Runnable type execution priority /// diff --git a/packages/yew/src/tests/layout_tests.rs b/packages/yew/src/tests/layout_tests.rs index 283d78566ab..744651bafcc 100644 --- a/packages/yew/src/tests/layout_tests.rs +++ b/packages/yew/src/tests/layout_tests.rs @@ -1,4 +1,5 @@ use crate::html::{AnyScope, Scope}; +use crate::scheduler; use crate::virtual_dom::{VDiff, VNode, VText}; use crate::{Component, Context, Html}; use gloo::console::log; @@ -51,6 +52,7 @@ pub fn diff_layouts(layouts: Vec>) { log!("Independently apply layout '{}'", layout.name); node.apply(&parent_scope, &parent_element, next_sibling.clone(), None); + scheduler::start_now(); assert_eq!( parent_element.inner_html(), format!("{}END", layout.expected), @@ -69,6 +71,7 @@ pub fn diff_layouts(layouts: Vec>) { next_sibling.clone(), Some(node), ); + scheduler::start_now(); assert_eq!( parent_element.inner_html(), format!("{}END", layout.expected), @@ -83,6 +86,7 @@ pub fn diff_layouts(layouts: Vec>) { next_sibling.clone(), Some(node_clone), ); + scheduler::start_now(); assert_eq!( parent_element.inner_html(), "END", @@ -103,6 +107,7 @@ pub fn diff_layouts(layouts: Vec>) { next_sibling.clone(), ancestor, ); + scheduler::start_now(); assert_eq!( parent_element.inner_html(), format!("{}END", layout.expected), @@ -123,6 +128,7 @@ pub fn diff_layouts(layouts: Vec>) { next_sibling.clone(), ancestor, ); + scheduler::start_now(); assert_eq!( parent_element.inner_html(), format!("{}END", layout.expected), @@ -134,6 +140,7 @@ pub fn diff_layouts(layouts: Vec>) { // Detach last layout empty_node.apply(&parent_scope, &parent_element, next_sibling, ancestor); + scheduler::start_now(); assert_eq!( parent_element.inner_html(), "END", diff --git a/packages/yew/src/virtual_dom/listeners.rs b/packages/yew/src/virtual_dom/listeners.rs index 0506ec680f8..c7af73c2391 100644 --- a/packages/yew/src/virtual_dom/listeners.rs +++ b/packages/yew/src/virtual_dom/listeners.rs @@ -522,9 +522,10 @@ mod tests { use web_sys::{Event, EventInit, MouseEvent}; wasm_bindgen_test_configure!(run_in_browser); - use crate::{html, html::TargetCast, AppHandle, Component, Context, Html}; + use crate::{html, html::TargetCast, scheduler, AppHandle, Component, Context, Html}; use gloo_utils::document; use wasm_bindgen::JsCast; + use yew::Callback; #[derive(Clone)] enum Message { @@ -545,15 +546,19 @@ mod tests { where C: Component, { + let link = ctx.link().clone(); + let onclick = Callback::from(move |_| { + link.send_message(Message::Action); + scheduler::start_now(); + }); + if state.stop_listening { html! { {state.action} } } else { html! { - + {state.action} } @@ -629,6 +634,7 @@ mod tests { let root = document().create_element("div").unwrap(); document().body().unwrap().append_child(&root).unwrap(); let app = crate::start_app_in_element::>(root); + scheduler::start_now(); (app, get_el_by_tag(tag)) } @@ -650,6 +656,8 @@ mod tests { assert_count(&el, 2); link.send_message(Message::StopListening); + scheduler::start_now(); + el.click(); assert_count(&el, 2); } @@ -663,7 +671,11 @@ mod tests { where C: Component, { - let onblur = ctx.link().callback(|_| Message::Action); + let link = ctx.link().clone(); + let onblur = Callback::from(move |_| { + link.send_message(Message::Action); + scheduler::start_now(); + }); html! { } } else { - let cb = ctx.link().callback(|_| Message::Action); + let link = ctx.link().clone(); + let cb = Callback::from(move |_| { + link.send_message(Message::Action); + scheduler::start_now(); + }); html! {
@@ -736,6 +752,7 @@ mod tests { assert_count(&el, 4); link.send_message(Message::StopListening); + scheduler::start_now(); el.click(); assert_count(&el, 4); } @@ -749,13 +766,22 @@ mod tests { where C: Component, { + let link = ctx.link().clone(); + let onclick = Callback::from(move |_| { + link.send_message(Message::Action); + scheduler::start_now(); + }); + + let link = ctx.link().clone(); + let onclick2 = Callback::from(move |e: MouseEvent| { + e.stop_propagation(); + link.send_message(Message::Action); + scheduler::start_now(); + }); + html! { -
- + @@ -786,12 +812,21 @@ mod tests { where C: Component, { + let link = ctx.link().clone(); + let onclick = Callback::from(move |_| { + link.send_message(Message::Action); + scheduler::start_now(); + }); + + let link = ctx.link().clone(); + let onclick2 = Callback::from(move |e: MouseEvent| { + e.stop_propagation(); + link.send_message(Message::Action); + scheduler::start_now(); + }); html! { -
-
+
+
{state.action} @@ -831,19 +866,23 @@ mod tests {
} } else { + let link = ctx.link().clone(); + let onchange = Callback::from(move |e: web_sys::Event| { + let el: web_sys::HtmlInputElement = e.target_unchecked_into(); + link.send_message(Message::SetText(el.value())); + scheduler::start_now(); + }); + + let link = ctx.link().clone(); + let oninput = Callback::from(move |e: web_sys::InputEvent| { + let el: web_sys::HtmlInputElement = e.target_unchecked_into(); + link.send_message(Message::SetText(el.value())); + scheduler::start_now(); + }); + html! {
- +

{state.text.clone()}

} @@ -860,6 +899,8 @@ mod tests { input_el.set_value(s); if s == &"baz" { link.send_message(Message::StopListening); + scheduler::start_now(); + s = &"bar"; } input_el diff --git a/packages/yew/src/virtual_dom/vcomp.rs b/packages/yew/src/virtual_dom/vcomp.rs index df64db87772..0587a0ab9b6 100644 --- a/packages/yew/src/virtual_dom/vcomp.rs +++ b/packages/yew/src/virtual_dom/vcomp.rs @@ -9,17 +9,20 @@ use std::borrow::Borrow; use std::fmt; use std::ops::Deref; use std::rc::Rc; +#[cfg(debug_assertions)] +use std::sync::atomic::{AtomicUsize, Ordering}; use web_sys::Element; +#[cfg(debug_assertions)] thread_local! { - #[cfg(debug_assertions)] - static EVENT_HISTORY: std::cell::RefCell>> + static EVENT_HISTORY: std::cell::RefCell>> = Default::default(); + static COMP_ID_COUNTER: AtomicUsize = AtomicUsize::new(0); } /// Push [VComp] event to lifecycle debugging registry #[cfg(debug_assertions)] -pub(crate) fn log_event(vcomp_id: u64, event: impl ToString) { +pub(crate) fn log_event(vcomp_id: usize, event: impl ToString) { EVENT_HISTORY.with(|h| { h.borrow_mut() .entry(vcomp_id) @@ -30,7 +33,7 @@ pub(crate) fn log_event(vcomp_id: u64, event: impl ToString) { /// Get [VComp] event log from lifecycle debugging registry #[cfg(debug_assertions)] -pub(crate) fn get_event_log(vcomp_id: u64) -> Vec { +pub(crate) fn get_event_log(vcomp_id: usize) -> Vec { EVENT_HISTORY.with(|h| { h.borrow() .get(&vcomp_id) @@ -47,9 +50,8 @@ pub struct VComp { pub(crate) node_ref: NodeRef, pub(crate) key: Option, - /// Used for debug logging #[cfg(debug_assertions)] - pub(crate) id: u64, + pub(crate) id: usize, } impl Clone for VComp { @@ -139,17 +141,7 @@ impl VComp { key, #[cfg(debug_assertions)] - id: { - thread_local! { - static ID_COUNTER: std::cell::RefCell = Default::default(); - } - - ID_COUNTER.with(|c| { - let c = &mut *c.borrow_mut(); - *c += 1; - *c - }) - }, + id: Self::next_id(), } } @@ -171,6 +163,11 @@ impl VComp { ); }) } + + #[cfg(debug_assertions)] + pub(crate) fn next_id() -> usize { + COMP_ID_COUNTER.with(|m| m.fetch_add(1, Ordering::Relaxed)) + } } trait Mountable { @@ -197,7 +194,7 @@ struct PropsWrapper { } impl PropsWrapper { - pub fn new(props: Rc) -> Self { + fn new(props: Rc) -> Self { Self { props } } } @@ -327,6 +324,7 @@ mod feat_ssr { #[cfg(test)] mod tests { use super::*; + use crate::scheduler; use crate::{html, Children, Component, Context, Html, NodeRef, Properties}; use gloo_utils::document; use web_sys::Node; @@ -372,6 +370,7 @@ mod tests { let mut ancestor = html! { }; ancestor.apply(&parent_scope, &parent_element, NodeRef::default(), None); + scheduler::start_now(); for _ in 0..10000 { let mut node = html! { }; @@ -381,6 +380,7 @@ mod tests { NodeRef::default(), Some(ancestor), ); + scheduler::start_now(); ancestor = node; } } @@ -534,6 +534,7 @@ mod tests { parent.set_inner_html(""); node.apply(scope, parent, NodeRef::default(), None); + scheduler::start_now(); parent.inner_html() } @@ -593,9 +594,11 @@ mod tests { let node_ref = NodeRef::default(); let mut elem: VNode = html! { }; elem.apply(&scope, &parent, NodeRef::default(), None); + scheduler::start_now(); let parent_node = parent.deref(); assert_eq!(node_ref.get(), parent_node.first_child()); elem.detach(&parent, false); + scheduler::start_now(); assert!(node_ref.get().is_none()); } } @@ -609,10 +612,8 @@ mod layout_tests { use crate::{Children, Component, Context, Html, Properties}; use std::marker::PhantomData; - #[cfg(feature = "wasm_test")] use wasm_bindgen_test::{wasm_bindgen_test as test, wasm_bindgen_test_configure}; - #[cfg(feature = "wasm_test")] wasm_bindgen_test_configure!(run_in_browser); struct Comp { diff --git a/packages/yew/tests/mod.rs b/packages/yew/tests/mod.rs index 6ea61c78267..a4ad6656a68 100644 --- a/packages/yew/tests/mod.rs +++ b/packages/yew/tests/mod.rs @@ -1,13 +1,15 @@ mod common; use common::obtain_result; +use gloo::timers::future::sleep; +use std::time::Duration; use wasm_bindgen_test::*; use yew::prelude::*; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] -fn props_are_passed() { +async fn props_are_passed() { #[derive(Properties, Clone, PartialEq)] struct PropsPassedFunctionProps { value: String, @@ -29,6 +31,8 @@ fn props_are_passed() { value: "props".to_string(), }, ); + + sleep(Duration::ZERO).await; let result = obtain_result(); assert_eq!(result.as_str(), "done"); } diff --git a/packages/yew/tests/suspense.rs b/packages/yew/tests/suspense.rs index 667458bc475..2691ff8ff18 100644 --- a/packages/yew/tests/suspense.rs +++ b/packages/yew/tests/suspense.rs @@ -119,6 +119,8 @@ async fn suspense_works() { .unwrap() .click(); + TimeoutFuture::new(0).await; + gloo_utils::document() .query_selector(".increase") .unwrap() @@ -127,6 +129,8 @@ async fn suspense_works() { .unwrap() .click(); + TimeoutFuture::new(1).await; + let result = obtain_result(); assert_eq!( result.as_str(), @@ -542,6 +546,8 @@ async fn effects_not_run_when_suspended() { .unwrap() .click(); + TimeoutFuture::new(0).await; + gloo_utils::document() .query_selector(".increase") .unwrap() @@ -550,6 +556,8 @@ async fn effects_not_run_when_suspended() { .unwrap() .click(); + TimeoutFuture::new(0).await; + let result = obtain_result(); assert_eq!( result.as_str(), diff --git a/packages/yew/tests/use_context.rs b/packages/yew/tests/use_context.rs index 0f655c39692..8ab7dba3d1d 100644 --- a/packages/yew/tests/use_context.rs +++ b/packages/yew/tests/use_context.rs @@ -1,14 +1,16 @@ mod common; use common::obtain_result_by_id; +use gloo::timers::future::sleep; use std::rc::Rc; +use std::time::Duration; use wasm_bindgen_test::*; use yew::prelude::*; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] -fn use_context_scoping_works() { +async fn use_context_scoping_works() { #[derive(Clone, Debug, PartialEq)] struct ExampleContext(String); @@ -62,12 +64,15 @@ fn use_context_scoping_works() { yew::start_app_in_element::( gloo_utils::document().get_element_by_id("output").unwrap(), ); + + sleep(Duration::ZERO).await; + let result: String = obtain_result_by_id("result"); assert_eq!("correct", result); } #[wasm_bindgen_test] -fn use_context_works_with_multiple_types() { +async fn use_context_works_with_multiple_types() { #[derive(Clone, Debug, PartialEq)] struct ContextA(u32); #[derive(Clone, Debug, PartialEq)] @@ -141,10 +146,12 @@ fn use_context_works_with_multiple_types() { yew::start_app_in_element::( gloo_utils::document().get_element_by_id("output").unwrap(), ); + + sleep(Duration::ZERO).await; } #[wasm_bindgen_test] -fn use_context_update_works() { +async fn use_context_update_works() { #[derive(Clone, Debug, PartialEq)] struct MyContext(String); @@ -239,6 +246,8 @@ fn use_context_update_works() { gloo_utils::document().get_element_by_id("output").unwrap(), ); + sleep(Duration::ZERO).await; + // 1 initial render + 3 update steps assert_eq!(obtain_result_by_id("test-0"), "total: 4"); diff --git a/packages/yew/tests/use_effect.rs b/packages/yew/tests/use_effect.rs index 594d607dcb7..1d701a98122 100644 --- a/packages/yew/tests/use_effect.rs +++ b/packages/yew/tests/use_effect.rs @@ -1,15 +1,17 @@ mod common; use common::obtain_result; +use gloo::timers::future::sleep; use std::ops::{Deref, DerefMut}; use std::rc::Rc; +use std::time::Duration; use wasm_bindgen_test::*; use yew::prelude::*; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] -fn use_effect_destroys_on_component_drop() { +async fn use_effect_destroys_on_component_drop() { #[derive(Properties, Clone)] struct WrapperProps { destroy_called: Rc, @@ -68,11 +70,14 @@ fn use_effect_destroys_on_component_drop() { destroy_called: Rc::new(move || *destroy_counter_c.borrow_mut().deref_mut() += 1), }, ); + + sleep(Duration::ZERO).await; + assert_eq!(1, *destroy_counter.borrow().deref()); } #[wasm_bindgen_test] -fn use_effect_works_many_times() { +async fn use_effect_works_many_times() { #[function_component(UseEffectComponent)] fn use_effect_comp() -> Html { let counter = use_state(|| 0); @@ -100,12 +105,14 @@ fn use_effect_works_many_times() { yew::start_app_in_element::( gloo_utils::document().get_element_by_id("output").unwrap(), ); + + sleep(Duration::ZERO).await; let result = obtain_result(); assert_eq!(result.as_str(), "4"); } #[wasm_bindgen_test] -fn use_effect_works_once() { +async fn use_effect_works_once() { #[function_component(UseEffectComponent)] fn use_effect_comp() -> Html { let counter = use_state(|| 0); @@ -131,12 +138,15 @@ fn use_effect_works_once() { yew::start_app_in_element::( gloo_utils::document().get_element_by_id("output").unwrap(), ); + sleep(Duration::ZERO).await; + let result = obtain_result(); + assert_eq!(result.as_str(), "1"); } #[wasm_bindgen_test] -fn use_effect_refires_on_dependency_change() { +async fn use_effect_refires_on_dependency_change() { #[function_component(UseEffectComponent)] fn use_effect_comp() -> Html { let number_ref = use_mut_ref(|| 0); @@ -175,6 +185,8 @@ fn use_effect_refires_on_dependency_change() { yew::start_app_in_element::( gloo_utils::document().get_element_by_id("output").unwrap(), ); + + sleep(Duration::ZERO).await; let result: String = obtain_result(); assert_eq!(result.as_str(), "11"); diff --git a/packages/yew/tests/use_memo.rs b/packages/yew/tests/use_memo.rs index 7dd39aac64b..927e9bc760f 100644 --- a/packages/yew/tests/use_memo.rs +++ b/packages/yew/tests/use_memo.rs @@ -3,13 +3,15 @@ use std::sync::atomic::{AtomicBool, Ordering}; mod common; use common::obtain_result; +use gloo::timers::future::sleep; +use std::time::Duration; use wasm_bindgen_test::*; use yew::prelude::*; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] -fn use_memo_works() { +async fn use_memo_works() { #[function_component(UseMemoComponent)] fn use_memo_comp() -> Html { let state = use_state(|| 0); @@ -48,6 +50,8 @@ fn use_memo_works() { gloo_utils::document().get_element_by_id("output").unwrap(), ); + sleep(Duration::ZERO).await; + let result = obtain_result(); assert_eq!(result.as_str(), "true"); } diff --git a/packages/yew/tests/use_reducer.rs b/packages/yew/tests/use_reducer.rs index 2d6454bc735..4fbab648ed8 100644 --- a/packages/yew/tests/use_reducer.rs +++ b/packages/yew/tests/use_reducer.rs @@ -1,7 +1,9 @@ use std::collections::HashSet; use std::rc::Rc; +use gloo::timers::future::sleep; use gloo_utils::document; +use std::time::Duration; use wasm_bindgen::JsCast; use wasm_bindgen_test::*; use web_sys::HtmlElement; @@ -30,7 +32,7 @@ impl Reducible for CounterState { } #[wasm_bindgen_test] -fn use_reducer_works() { +async fn use_reducer_works() { #[function_component(UseReducerComponent)] fn use_reducer_comp() -> Html { let counter = use_reducer(|| CounterState { counter: 10 }); @@ -55,6 +57,7 @@ fn use_reducer_works() { yew::start_app_in_element::( gloo_utils::document().get_element_by_id("output").unwrap(), ); + sleep(Duration::ZERO).await; let result = obtain_result(); assert_eq!(result.as_str(), "11"); @@ -76,7 +79,7 @@ impl Reducible for ContentState { } #[wasm_bindgen_test] -fn use_reducer_eq_works() { +async fn use_reducer_eq_works() { #[function_component(UseReducerComponent)] fn use_reducer_comp() -> Html { let content = use_reducer_eq(|| ContentState { @@ -113,6 +116,7 @@ fn use_reducer_eq_works() { yew::start_app_in_element::( document().get_element_by_id("output").unwrap(), ); + sleep(Duration::ZERO).await; let result = obtain_result(); assert_eq!(result.as_str(), "1"); @@ -122,6 +126,7 @@ fn use_reducer_eq_works() { .unwrap() .unchecked_into::() .click(); + sleep(Duration::ZERO).await; let result = obtain_result(); assert_eq!(result.as_str(), "2"); @@ -131,6 +136,7 @@ fn use_reducer_eq_works() { .unwrap() .unchecked_into::() .click(); + sleep(Duration::ZERO).await; let result = obtain_result(); assert_eq!(result.as_str(), "2"); @@ -140,6 +146,7 @@ fn use_reducer_eq_works() { .unwrap() .unchecked_into::() .click(); + sleep(Duration::ZERO).await; let result = obtain_result(); assert_eq!(result.as_str(), "3"); @@ -149,6 +156,7 @@ fn use_reducer_eq_works() { .unwrap() .unchecked_into::() .click(); + sleep(Duration::ZERO).await; let result = obtain_result(); assert_eq!(result.as_str(), "3"); diff --git a/packages/yew/tests/use_ref.rs b/packages/yew/tests/use_ref.rs index 662f09206a6..f08be26ebd7 100644 --- a/packages/yew/tests/use_ref.rs +++ b/packages/yew/tests/use_ref.rs @@ -1,14 +1,16 @@ mod common; use common::obtain_result; +use gloo::timers::future::sleep; use std::ops::DerefMut; +use std::time::Duration; use wasm_bindgen_test::*; use yew::prelude::*; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] -fn use_ref_works() { +async fn use_ref_works() { #[function_component(UseRefComponent)] fn use_ref_comp() -> Html { let ref_example = use_mut_ref(|| 0); @@ -29,6 +31,7 @@ fn use_ref_works() { yew::start_app_in_element::( gloo_utils::document().get_element_by_id("output").unwrap(), ); + sleep(Duration::ZERO).await; let result = obtain_result(); assert_eq!(result.as_str(), "true"); diff --git a/packages/yew/tests/use_state.rs b/packages/yew/tests/use_state.rs index 1dd59b1de69..911fdd06fd4 100644 --- a/packages/yew/tests/use_state.rs +++ b/packages/yew/tests/use_state.rs @@ -1,13 +1,15 @@ mod common; use common::obtain_result; +use gloo::timers::future::sleep; +use std::time::Duration; use wasm_bindgen_test::*; use yew::prelude::*; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); #[wasm_bindgen_test] -fn use_state_works() { +async fn use_state_works() { #[function_component(UseComponent)] fn use_state_comp() -> Html { let counter = use_state(|| 0); @@ -26,12 +28,13 @@ fn use_state_works() { yew::start_app_in_element::( gloo_utils::document().get_element_by_id("output").unwrap(), ); + sleep(Duration::ZERO).await; let result = obtain_result(); assert_eq!(result.as_str(), "5"); } #[wasm_bindgen_test] -fn multiple_use_state_setters() { +async fn multiple_use_state_setters() { #[function_component(UseComponent)] fn use_state_comp() -> Html { let counter = use_state(|| 0); @@ -67,12 +70,13 @@ fn multiple_use_state_setters() { yew::start_app_in_element::( gloo_utils::document().get_element_by_id("output").unwrap(), ); + sleep(Duration::ZERO).await; let result = obtain_result(); assert_eq!(result.as_str(), "11"); } #[wasm_bindgen_test] -fn use_state_eq_works() { +async fn use_state_eq_works() { use std::sync::atomic::{AtomicUsize, Ordering}; static RENDER_COUNT: AtomicUsize = AtomicUsize::new(0); @@ -94,6 +98,7 @@ fn use_state_eq_works() { yew::start_app_in_element::( gloo_utils::document().get_element_by_id("output").unwrap(), ); + sleep(Duration::ZERO).await; let result = obtain_result(); assert_eq!(result.as_str(), "1"); assert_eq!(RENDER_COUNT.load(Ordering::Relaxed), 2); diff --git a/website/docs/migration-guides/yew/from-0_19_0-to-0_20_0.mdx b/website/docs/migration-guides/yew/from-0_19_0-to-0_20_0.mdx index abccf2959b7..dd511251d35 100644 --- a/website/docs/migration-guides/yew/from-0_19_0-to-0_20_0.mdx +++ b/website/docs/migration-guides/yew/from-0_19_0-to-0_20_0.mdx @@ -15,3 +15,31 @@ The Function Components and Hooks API are re-implemented with a different mechan - Hooks will now report compile errors if they are not called from the top level of a function component or a user defined hook. The limitation existed in the previous version of Yew as well. In this version, It is reported as a compile time error. + +## Automatic Message Batching + +The scheduler now shedules its start to the end of the browser event loop. +All messages queued during in the meantime will be run in batch. +The running order of messages between components are no longer guaranteed, but +messages sent to the same component is still acknowledged in an FIFO order. +If multiple updates will result in a render, the component will be rendered +once. + +:::info What this means to developers? + +For struct components, this means that if you send 2 messages to 2 different +components, they will not be guaranteed to be seen in the same order they are +sent. If you send 2 messages to the same component, they will still be passed +to the component in the order they are sent. The messages are not sent to the +component immediately so you should not assume that when the component receives +a message it still has the same state at the time the message is created. + +For function components, if you store states with `use_state(_eq)` +and the new value of that state depends on the previous value, +you may want to switch to `use_reducer(_eq)`. The new value of the state will +not be visible / acknowledged until the next time the component is rendered. +The reducer action works similar to messages for struct components and +will be sent to the reducer function in the same order as they are dispatched. +The reducer function can see all previous changes at the time they are run. + +:::