Bläddra i källkod

chore: make warnings go away

Jonathan Kelley 2 år sedan
förälder
incheckning
7c3d308ab5

+ 1 - 0
packages/core/Cargo.toml

@@ -34,6 +34,7 @@ indexmap = "1.7"
 # Serialize the Edits for use in Webview/Liveview instances
 serde = { version = "1", features = ["derive"], optional = true }
 anyhow = "1.0.66"
+bumpslab = "0.1.0"
 
 [dev-dependencies]
 tokio = { version = "*", features = ["full"] }

+ 1 - 3
packages/core/src/any_props.rs

@@ -18,7 +18,6 @@ pub(crate) struct VProps<'a, P, A, F: ComponentReturn<'a, A> = Element<'a>> {
     pub render_fn: fn(Scope<'a, P>) -> F,
     pub memo: unsafe fn(&P, &P) -> bool,
     pub props: P,
-    // pub props: PropsAllocation<P>,
     _marker: PhantomData<A>,
 }
 
@@ -35,7 +34,6 @@ where
             render_fn,
             memo,
             props,
-            // props: PropsAllocation::Borrowed(props),
             _marker: PhantomData,
         }
     }
@@ -60,7 +58,7 @@ where
     }
 
     fn render(&'a self, cx: &'a ScopeState) -> RenderReturn<'a> {
-        let scope = cx.bump().alloc(Scoped {
+        let scope: &mut Scoped<P> = cx.bump().alloc(Scoped {
             props: &self.props,
             scope: cx,
         });

+ 62 - 44
packages/core/src/arena.rs

@@ -1,10 +1,14 @@
-use crate::{nodes::VNode, virtual_dom::VirtualDom, Mutations, ScopeId};
+use crate::{
+    factory::RenderReturn, nodes::VNode, virtual_dom::VirtualDom, AttributeValue, DynamicNode,
+    ScopeId, VFragment,
+};
+use bumpalo::boxed::Box as BumpBox;
 
 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
 #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
 pub struct ElementId(pub usize);
 
-pub struct ElementRef {
+pub(crate) struct ElementRef {
     // the pathway of the real element inside the template
     pub path: ElementPath,
 
@@ -19,7 +23,7 @@ pub enum ElementPath {
 }
 
 impl ElementRef {
-    pub fn null() -> Self {
+    pub(crate) fn null() -> Self {
         Self {
             template: std::ptr::null_mut(),
             path: ElementPath::Root(0),
@@ -28,74 +32,88 @@ impl ElementRef {
 }
 
 impl<'b> VirtualDom {
-    pub fn next_element(&mut self, template: &VNode, path: &'static [u8]) -> ElementId {
+    pub(crate) fn next_element(&mut self, template: &VNode, path: &'static [u8]) -> ElementId {
         let entry = self.elements.vacant_entry();
         let id = entry.key();
-
         entry.insert(ElementRef {
             template: template as *const _ as *mut _,
             path: ElementPath::Deep(path),
         });
-
-        println!("Claiming {}", id);
-
         ElementId(id)
     }
 
-    pub fn next_root(&mut self, template: &VNode, path: usize) -> ElementId {
+    pub(crate) fn next_root(&mut self, template: &VNode, path: usize) -> ElementId {
         let entry = self.elements.vacant_entry();
         let id = entry.key();
-
         entry.insert(ElementRef {
             template: template as *const _ as *mut _,
             path: ElementPath::Root(path),
         });
-
-        println!("Claiming {}", id);
-
         ElementId(id)
     }
 
-    pub fn cleanup_element(&mut self, id: ElementId) {
-        self.elements.remove(id.0);
+    pub(crate) fn reclaim(&mut self, el: ElementId) {
+        self.try_reclaim(el)
+            .unwrap_or_else(|| panic!("cannot reclaim {:?}", el));
     }
 
-    pub fn drop_scope(&mut self, id: ScopeId) {
-        // let scope = self.scopes.get(id.0).unwrap();
+    pub(crate) fn try_reclaim(&mut self, el: ElementId) -> Option<ElementRef> {
+        assert_ne!(el, ElementId(0));
+        self.elements.try_remove(el.0)
+    }
 
-        // let root = scope.root_node();
-        // let root = unsafe { std::mem::transmute(root) };
+    // Drop a scope and all its children
+    pub(crate) fn drop_scope(&mut self, id: ScopeId) {
+        let scope = self.scopes.get(id.0).unwrap();
 
-        // self.drop_template(root, false);
-        todo!()
-    }
+        if let Some(root) = scope.as_ref().try_root_node() {
+            let root = unsafe { root.extend_lifetime_ref() };
+            match root {
+                RenderReturn::Sync(Ok(node)) => self.drop_scope_inner(node),
+                _ => {}
+            }
+        }
 
-    pub fn reclaim(&mut self, el: ElementId) {
-        assert_ne!(el, ElementId(0));
-        self.elements.remove(el.0);
+        let scope = self.scopes.get(id.0).unwrap();
+
+        if let Some(root) = unsafe { scope.as_ref().previous_frame().try_load_node() } {
+            let root = unsafe { root.extend_lifetime_ref() };
+            match root {
+                RenderReturn::Sync(Ok(node)) => self.drop_scope_inner(node),
+                _ => {}
+            }
+        }
+
+        let scope = self.scopes.get(id.0).unwrap().as_ref();
+
+        // Drop all the hooks once the children are dropped
+        // this means we'll drop hooks bottom-up
+        for hook in scope.hook_list.borrow_mut().drain(..) {
+            drop(unsafe { BumpBox::from_raw(hook) });
+        }
     }
 
-    pub fn drop_template(
-        &mut self,
-        mutations: &mut Mutations,
-        template: &'b VNode<'b>,
-        gen_roots: bool,
-    ) {
-        // for node in template.dynamic_nodes.iter() {
-        //     match node {
-        //         DynamicNode::Text { id, .. } => {}
-
-        //         DynamicNode::Component { .. } => {
-        //             todo!()
-        //         }
-
-        //         DynamicNode::Fragment { inner, nodes } => {}
-        //         DynamicNode::Placeholder(_) => todo!(),
-        //         _ => todo!(),
-        //     }
-        // }
+    fn drop_scope_inner(&mut self, node: &VNode) {
+        for attr in node.dynamic_attrs {
+            if let AttributeValue::Listener(l) = &attr.value {
+                l.borrow_mut().take();
+            }
+        }
+
+        for (idx, _) in node.template.roots.iter().enumerate() {
+            match node.dynamic_root(idx) {
+                Some(DynamicNode::Component(c)) => self.drop_scope(c.scope.get().unwrap()),
+                Some(DynamicNode::Fragment(VFragment::NonEmpty(nodes))) => {
+                    for node in *nodes {
+                        self.drop_scope_inner(node);
+                    }
+                }
+                _ => {}
+            }
+        }
     }
 }
+
 impl ElementPath {
     pub(crate) fn is_ascendant(&self, big: &&[u8]) -> bool {
         match *self {

+ 11 - 10
packages/core/src/bump_frame.rs

@@ -2,9 +2,9 @@ use crate::factory::RenderReturn;
 use bumpalo::Bump;
 use std::cell::Cell;
 
-pub struct BumpFrame {
+pub(crate) struct BumpFrame {
     pub bump: Bump,
-    pub node: Cell<*mut RenderReturn<'static>>,
+    pub node: Cell<*const RenderReturn<'static>>,
 }
 
 impl BumpFrame {
@@ -12,17 +12,18 @@ impl BumpFrame {
         let bump = Bump::with_capacity(capacity);
         Self {
             bump,
-            node: Cell::new(std::ptr::null_mut()),
+            node: Cell::new(std::ptr::null()),
         }
     }
 
-    pub fn reset(&mut self) {
-        self.bump.reset();
-        self.node.set(std::ptr::null_mut());
-    }
-
     /// Creates a new lifetime out of thin air
-    pub unsafe fn load_node<'b>(&self) -> &'b RenderReturn<'b> {
-        unsafe { std::mem::transmute(&*self.node.get()) }
+    pub unsafe fn try_load_node<'b>(&self) -> Option<&'b RenderReturn<'b>> {
+        let node = self.node.get();
+
+        if node.is_null() {
+            return None;
+        }
+
+        unsafe { std::mem::transmute(&*node) }
     }
 }

+ 38 - 21
packages/core/src/create.rs

@@ -5,9 +5,9 @@ use crate::mutations::Mutation::*;
 use crate::nodes::VNode;
 use crate::nodes::{DynamicNode, TemplateNode};
 use crate::virtual_dom::VirtualDom;
-use crate::{AttributeValue, ElementId, ScopeId, SuspenseContext, TemplateAttribute};
+use crate::{AttributeValue, ScopeId, SuspenseContext, TemplateAttribute};
 
-impl<'b: 'static> VirtualDom {
+impl<'b> VirtualDom {
     /// Create a new template [`VNode`] and write it to the [`Mutations`] buffer.
     ///
     /// This method pushes the ScopeID to the internal scopestack and returns the number of nodes created.
@@ -44,7 +44,14 @@ impl<'b: 'static> VirtualDom {
                         DynamicNode::Text(VText { id: slot, value }) => {
                             let id = self.next_element(template, template.template.node_paths[*id]);
                             slot.set(id);
-                            self.mutations.push(CreateTextNode { value, id });
+
+                            // Safety: we promise not to re-alias this text later on after committing it to the mutation
+                            let unbounded_text = unsafe { std::mem::transmute(*value) };
+                            self.mutations.push(CreateTextNode {
+                                value: unbounded_text,
+                                id,
+                            });
+
                             1
                         }
 
@@ -96,16 +103,24 @@ impl<'b: 'static> VirtualDom {
                             let attribute = template.dynamic_attrs.get(attr_id).unwrap();
                             attribute.mounted_element.set(id);
 
+                            // Safety: we promise not to re-alias this text later on after committing it to the mutation
+                            let unbounded_name = unsafe { std::mem::transmute(attribute.name) };
+
                             match &attribute.value {
-                                AttributeValue::Text(value) => self.mutations.push(SetAttribute {
-                                    name: attribute.name,
-                                    value: *value,
-                                    ns: attribute.namespace,
-                                    id,
-                                }),
+                                AttributeValue::Text(value) => {
+                                    // Safety: we promise not to re-alias this text later on after committing it to the mutation
+                                    let unbounded_value = unsafe { std::mem::transmute(*value) };
+
+                                    self.mutations.push(SetAttribute {
+                                        name: unbounded_name,
+                                        value: unbounded_value,
+                                        ns: attribute.namespace,
+                                        id,
+                                    })
+                                }
                                 AttributeValue::Bool(value) => {
                                     self.mutations.push(SetBoolAttribute {
-                                        name: attribute.name,
+                                        name: unbounded_name,
                                         value: *value,
                                         id,
                                     })
@@ -113,7 +128,7 @@ impl<'b: 'static> VirtualDom {
                                 AttributeValue::Listener(_) => {
                                     self.mutations.push(NewEventListener {
                                         // all listeners start with "on"
-                                        event_name: &attribute.name[2..],
+                                        event_name: &unbounded_name[2..],
                                         scope: cur_scope,
                                         id,
                                     })
@@ -315,11 +330,14 @@ impl<'b: 'static> VirtualDom {
         // Make sure the text node is assigned to the correct element
         text.id.set(new_id);
 
+        // Safety: we promise not to re-alias this text later on after committing it to the mutation
+        let value = unsafe { std::mem::transmute(text.value) };
+
         // Add the mutation to the list
         self.mutations.push(HydrateText {
             id: new_id,
             path: &template.template.node_paths[idx][1..],
-            value: text.value,
+            value,
         });
 
         // Since we're hydrating an existing node, we don't create any new nodes
@@ -356,18 +374,20 @@ impl<'b: 'static> VirtualDom {
         }
     }
 
-    fn create_component_node(
+    pub(super) fn create_component_node(
         &mut self,
         template: &'b VNode<'b>,
         component: &'b VComponent<'b>,
         idx: usize,
     ) -> usize {
-        let props = component.props.replace(None).unwrap();
+        let props = component
+            .props
+            .replace(None)
+            .expect("Props to always exist when a component is being created");
 
-        let prop_ptr = unsafe { std::mem::transmute(props.as_ref()) };
-        let scope = self.new_scope(prop_ptr).id;
+        let unbounded_props = unsafe { std::mem::transmute(props) };
 
-        component.props.replace(Some(props));
+        let scope = self.new_scope(unbounded_props).id;
         component.scope.set(Some(scope));
 
         let return_nodes = unsafe { self.run_scope(scope).extend_lifetime_ref() };
@@ -375,10 +395,7 @@ impl<'b: 'static> VirtualDom {
         use RenderReturn::*;
 
         match return_nodes {
-            Sync(Ok(t)) => {
-                self.mount_component(scope, template, t, idx)
-                // self.create(t)
-            }
+            Sync(Ok(t)) => self.mount_component(scope, template, t, idx),
             Sync(Err(_e)) => todo!("Propogate error upwards"),
             Async(_) => self.mount_component_placeholder(template, idx, scope),
         }

+ 101 - 65
packages/core/src/diff.rs

@@ -13,39 +13,29 @@ use fxhash::{FxHashMap, FxHashSet};
 use std::cell::Cell;
 use DynamicNode::*;
 
-impl<'b: 'static> VirtualDom {
+impl<'b> VirtualDom {
     pub fn diff_scope(&mut self, scope: ScopeId) {
         let scope_state = &mut self.scopes[scope.0];
 
-        // Load the old and new bump arenas
-        let cur_arena = scope_state.current_frame();
-        let prev_arena = scope_state.previous_frame();
-
-        // Make sure the nodes arent null (they've been set properly)
-        // This is a rough check to make sure we're not entering any UB
-        assert_ne!(
-            cur_arena.node.get(),
-            std::ptr::null_mut(),
-            "Call rebuild before diffing"
-        );
-        assert_ne!(
-            prev_arena.node.get(),
-            std::ptr::null_mut(),
-            "Call rebuild before diffing"
-        );
-
         self.scope_stack.push(scope);
         unsafe {
-            let cur_arena = cur_arena.load_node();
-            let prev_arena = prev_arena.load_node();
-            self.diff_maybe_node(prev_arena, cur_arena);
+            // Load the old and new bump arenas
+            let old = scope_state
+                .previous_frame()
+                .try_load_node()
+                .expect("Call rebuild before diffing");
+            let new = scope_state
+                .current_frame()
+                .try_load_node()
+                .expect("Call rebuild before diffing");
+            self.diff_maybe_node(old, new);
         }
         self.scope_stack.pop();
     }
 
-    fn diff_maybe_node(&mut self, left: &'b RenderReturn<'b>, right: &'b RenderReturn<'b>) {
+    fn diff_maybe_node(&mut self, old: &'b RenderReturn<'b>, new: &'b RenderReturn<'b>) {
         use RenderReturn::{Async, Sync};
-        match (left, right) {
+        match (old, new) {
             (Sync(Ok(l)), Sync(Ok(r))) => self.diff_node(l, r),
 
             // Err cases
@@ -62,10 +52,10 @@ impl<'b: 'static> VirtualDom {
         }
     }
 
-    fn diff_ok_to_err(&mut self, l: &'b VNode<'b>, e: &anyhow::Error) {
+    fn diff_ok_to_err(&mut self, _l: &'b VNode<'b>, _e: &anyhow::Error) {
         todo!("Not yet handling error rollover")
     }
-    fn diff_err_to_ok(&mut self, e: &anyhow::Error, l: &'b VNode<'b>) {
+    fn diff_err_to_ok(&mut self, _e: &anyhow::Error, _l: &'b VNode<'b>) {
         todo!("Not yet handling error rollover")
     }
 
@@ -88,11 +78,13 @@ impl<'b: 'static> VirtualDom {
                 // todo: add more types of attribute values
                 match right_attr.value {
                     AttributeValue::Text(text) => {
+                        let name = unsafe { std::mem::transmute(left_attr.name) };
+                        let value = unsafe { std::mem::transmute(text) };
                         self.mutations.push(Mutation::SetAttribute {
                             id: left_attr.mounted_element.get(),
-                            name: left_attr.name,
-                            value: text,
                             ns: right_attr.namespace,
+                            name,
+                            value,
                         });
                     }
                     // todo: more types of attribute values
@@ -101,15 +93,18 @@ impl<'b: 'static> VirtualDom {
             }
         }
 
-        for (left_node, right_node) in left_template
+        for (idx, (left_node, right_node)) in left_template
             .dynamic_nodes
             .iter()
             .zip(right_template.dynamic_nodes.iter())
+            .enumerate()
         {
             match (left_node, right_node) {
                 (Text(left), Text(right)) => self.diff_vtext(left, right),
                 (Fragment(left), Fragment(right)) => self.diff_vfragment(left, right),
-                (Component(left), Component(right)) => self.diff_vcomponent(left, right),
+                (Component(left), Component(right)) => {
+                    self.diff_vcomponent(left, right, right_template, idx)
+                }
                 _ => self.replace(left_template, right_template),
             };
         }
@@ -124,24 +119,44 @@ impl<'b: 'static> VirtualDom {
         }
     }
 
-    fn diff_vcomponent(&mut self, left: &'b VComponent<'b>, right: &'b VComponent<'b>) {
-        // Due to how templates work, we should never get two different components. The only way we could enter
-        // this codepath is through "light_diff", but we check there that the pointers are the same
-        assert_eq!(left.render_fn, right.render_fn);
+    fn diff_vcomponent(
+        &mut self,
+        left: &'b VComponent<'b>,
+        right: &'b VComponent<'b>,
+        right_template: &'b VNode<'b>,
+        idx: usize,
+    ) {
+        // Replace components that have different render fns
+        if left.render_fn != right.render_fn {
+            dbg!(&left, &right);
+            let created = self.create_component_node(right_template, right, idx);
+            let head = unsafe {
+                self.scopes[left.scope.get().unwrap().0]
+                    .root_node()
+                    .extend_lifetime_ref()
+            };
+            let id = match head {
+                RenderReturn::Sync(Ok(node)) => self.replace_inner(node),
+                _ => todo!(),
+            };
+            self.mutations
+                .push(Mutation::ReplaceWith { id, m: created });
+            return;
+        }
 
         // Make sure the new vcomponent has the right scopeid associated to it
         let scope_id = left.scope.get().unwrap();
         right.scope.set(Some(scope_id));
 
         // copy out the box for both
-        let old = left.props.replace(None).unwrap();
+        let old = self.scopes[scope_id.0].props.as_ref();
         let new = right.props.replace(None).unwrap();
 
         // If the props are static, then we try to memoize by setting the new with the old
         // 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 left.static_props && unsafe { old.memoize(new.as_ref()) } {
-            return right.props.set(Some(old));
+            return;
         }
 
         // If the props are dynamic *or* the memoization failed, then we need to diff the props
@@ -198,7 +213,8 @@ impl<'b: 'static> VirtualDom {
             None => self.replace(left, right),
             Some(components) => components
                 .into_iter()
-                .for_each(|(l, r)| self.diff_vcomponent(l, r)),
+                .enumerate()
+                .for_each(|(idx, (l, r))| self.diff_vcomponent(l, r, right, idx)),
         }
     }
 
@@ -207,12 +223,12 @@ impl<'b: 'static> VirtualDom {
     /// This just moves the ID of the old node over to the new node, and then sets the text of the new node if it's
     /// different.
     fn diff_vtext(&mut self, left: &'b VText<'b>, right: &'b VText<'b>) {
-        right.id.set(left.id.get());
+        let id = left.id.get();
+
+        right.id.set(id);
         if left.value != right.value {
-            self.mutations.push(Mutation::SetText {
-                id: left.id.get(),
-                value: right.value,
-            });
+            let value = unsafe { std::mem::transmute(right.value) };
+            self.mutations.push(Mutation::SetText { id, value });
         }
     }
 
@@ -234,22 +250,25 @@ impl<'b: 'static> VirtualDom {
     }
 
     fn replace_nodes_with_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b Cell<ElementId>) {
-        // Remove the old nodes, except for onea
-        let first = self.replace_inner(&l[0]);
-        self.remove_nodes(&l[1..]);
-
+        // Create the placeholder first, ensuring we get a dedicated ID for the placeholder
         let placeholder = self.next_element(&l[0], &[]);
         r.set(placeholder);
-
         self.mutations
             .push(Mutation::CreatePlaceholder { id: placeholder });
+
+        // Remove the old nodes, except for onea
+        let first = self.replace_inner(&l[0]);
+        self.remove_nodes(&l[1..]);
+
         self.mutations
             .push(Mutation::ReplaceWith { id: first, m: 1 });
 
-        self.reclaim(first);
+        self.try_reclaim(first);
     }
 
-    // Remove all the top-level nodes, returning the firstmost root ElementId
+    /// Remove all the top-level nodes, returning the firstmost root ElementId
+    ///
+    /// All IDs will be garbage collected
     fn replace_inner(&mut self, node: &'b VNode<'b>) -> ElementId {
         let id = match node.dynamic_root(0) {
             None => node.root_ids[0].get(),
@@ -284,8 +303,13 @@ impl<'b: 'static> VirtualDom {
     ///
     /// Simply walks through the dynamic nodes
     fn clean_up_node(&mut self, node: &'b VNode<'b>) {
-        for node in node.dynamic_nodes {
-            match node {
+        for (idx, dyn_node) in node.dynamic_nodes.iter().enumerate() {
+            // Roots are cleaned up automatically?
+            if node.template.node_paths[idx].len() == 1 {
+                continue;
+            }
+
+            match dyn_node {
                 Component(comp) => {
                     let scope = comp.scope.get().unwrap();
                     match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
@@ -596,10 +620,9 @@ impl<'b: 'static> VirtualDom {
         // If none of the old keys are reused by the new children, then we remove all the remaining old children and
         // create the new children afresh.
         if shared_keys.is_empty() {
-            if let Some(first_old) = old.get(0) {
+            if let Some(_) = old.get(0) {
                 self.remove_nodes(&old[1..]);
-                let nodes_created = self.create_children(new);
-                self.replace_node_with_on_stack(first_old, nodes_created);
+                self.replace_many(&old[0], new);
             } else {
                 // I think this is wrong - why are we appending?
                 // only valid of the if there are no trailing elements
@@ -718,10 +741,6 @@ impl<'b: 'static> VirtualDom {
         }
     }
 
-    fn replace_node_with_on_stack(&mut self, old: &'b VNode<'b>, m: usize) {
-        todo!()
-    }
-
     /// Remove these nodes from the dom
     /// Wont generate mutations for the inner nodes
     fn remove_nodes(&mut self, nodes: &'b [VNode<'b>]) {
@@ -766,7 +785,7 @@ impl<'b: 'static> VirtualDom {
     }
 
     /// Push all the real nodes on the stack
-    fn push_all_real_nodes(&mut self, node: &VNode) -> usize {
+    fn push_all_real_nodes(&mut self, node: &'b VNode<'b>) -> usize {
         let mut onstack = 0;
 
         for (idx, _) in node.template.roots.iter().enumerate() {
@@ -785,7 +804,12 @@ impl<'b: 'static> VirtualDom {
                     }
                 }
                 Some(Component(comp)) => {
-                    todo!()
+                    let scope = comp.scope.get().unwrap();
+                    onstack +=
+                        match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
+                            RenderReturn::Sync(Ok(node)) => self.push_all_real_nodes(node),
+                            _ => todo!(),
+                        }
                 }
                 None => {
                     self.mutations.push(Mutation::PushRoot {
@@ -815,7 +839,7 @@ impl<'b: 'static> VirtualDom {
         self.mutations.push(Mutation::InsertAfter { id, m })
     }
 
-    fn find_first_element(&self, node: &VNode) -> ElementId {
+    fn find_first_element(&self, node: &'b VNode<'b>) -> ElementId {
         match node.dynamic_root(0) {
             None => node.root_ids[0].get(),
             Some(Text(t)) => t.id.get(),
@@ -823,7 +847,7 @@ impl<'b: 'static> VirtualDom {
             Some(Fragment(VFragment::Empty(t))) => t.get(),
             Some(Component(comp)) => {
                 let scope = comp.scope.get().unwrap();
-                match self.scopes[scope.0].root_node() {
+                match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
                     RenderReturn::Sync(Ok(t)) => self.find_first_element(t),
                     _ => todo!("cannot handle nonstandard nodes"),
                 }
@@ -831,7 +855,7 @@ impl<'b: 'static> VirtualDom {
         }
     }
 
-    fn find_last_element(&self, node: &VNode) -> ElementId {
+    fn find_last_element(&self, node: &'b VNode<'b>) -> ElementId {
         match node.dynamic_root(node.template.roots.len() - 1) {
             None => node.root_ids.last().unwrap().get(),
             Some(Text(t)) => t.id.get(),
@@ -839,7 +863,7 @@ impl<'b: 'static> VirtualDom {
             Some(Fragment(VFragment::Empty(t))) => t.get(),
             Some(Component(comp)) => {
                 let scope = comp.scope.get().unwrap();
-                match self.scopes[scope.0].root_node() {
+                match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } {
                     RenderReturn::Sync(Ok(t)) => self.find_last_element(t),
                     _ => todo!("cannot handle nonstandard nodes"),
                 }
@@ -855,7 +879,18 @@ impl<'b: 'static> VirtualDom {
             id: first,
             m: created,
         });
-        self.reclaim(id);
+        self.try_reclaim(id);
+    }
+
+    fn replace_many(&mut self, left: &'b VNode<'b>, right: &'b [VNode<'b>]) {
+        let first = self.find_first_element(left);
+        let id = self.replace_inner(left);
+        let created = self.create_children(right);
+        self.mutations.push(Mutation::ReplaceWith {
+            id: first,
+            m: created,
+        });
+        self.try_reclaim(id);
     }
 }
 
@@ -883,7 +918,7 @@ fn matching_components<'a>(
                 _ => return None,
             };
 
-            (l.render_fn == r.render_fn).then(|| (l, r))
+            Some((l, r))
         })
         .collect()
 }
@@ -894,6 +929,7 @@ fn matching_components<'a>(
 ///  - for text - we can use SetTextContent
 ///  - for clearning children we can use RemoveChildren
 ///  - for appending children we can use AppendChildren
+#[allow(dead_code)]
 fn is_dyn_node_only_child(node: &VNode, idx: usize) -> bool {
     let path = node.template.node_paths[idx];
 

+ 12 - 22
packages/core/src/error_boundary.rs

@@ -1,29 +1,19 @@
-use std::{cell::RefCell, rc::Rc};
+use std::cell::RefCell;
 
-use crate::{ScopeId, ScopeState};
+use crate::ScopeId;
 
-pub struct ErrorContext {
+/// A boundary that will capture any errors from child components
+#[allow(dead_code)]
+pub struct ErrorBoundary {
     error: RefCell<Option<(anyhow::Error, ScopeId)>>,
+    id: ScopeId,
 }
 
-/// Catch all errors from the children and bubble them up to this component
-///
-/// Returns the error and scope that caused the error
-pub fn use_catch_error(cx: &ScopeState) -> Option<&(anyhow::Error, ScopeId)> {
-    let err_ctx = use_error_context(cx);
-
-    let out = cx.use_hook(|| None);
-
-    if let Some(error) = err_ctx.error.take() {
-        *out = Some(error);
+impl ErrorBoundary {
+    pub fn new(id: ScopeId) -> Self {
+        Self {
+            error: RefCell::new(None),
+            id,
+        }
     }
-
-    out.as_ref()
-}
-
-/// Create a new error context at this component.
-///
-/// This component will start to catch any errors that occur in its children.
-pub fn use_error_context(cx: &ScopeState) -> &ErrorContext {
-    cx.use_hook(|| cx.provide_context(Rc::new(ErrorContext { error: None.into() })))
 }

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

@@ -50,58 +50,6 @@ impl<T: Debug> std::fmt::Debug for UiEvent<T> {
     }
 }
 
-/// Priority of Event Triggers.
-///
-/// Internally, Dioxus will abort work that's taking too long if new, more important work arrives. Unlike React, Dioxus
-/// won't be afraid to pause work or flush changes to the Real Dom. This is called "cooperative scheduling". Some Renderers
-/// implement this form of scheduling internally, however Dioxus will perform its own scheduling as well.
-///
-/// The ultimate goal of the scheduler is to manage latency of changes, prioritizing "flashier" changes over "subtler" changes.
-///
-/// React has a 5-tier priority system. However, they break things into "Continuous" and "Discrete" priority. For now,
-/// we keep it simple, and just use a 3-tier priority system.
-///
-/// - `NoPriority` = 0
-/// - `LowPriority` = 1
-/// - `NormalPriority` = 2
-/// - `UserBlocking` = 3
-/// - `HighPriority` = 4
-/// - `ImmediatePriority` = 5
-///
-/// We still have a concept of discrete vs continuous though - discrete events won't be batched, but continuous events will.
-/// This means that multiple "scroll" events will be processed in a single frame, but multiple "click" events will be
-/// flushed before proceeding. Multiple discrete events is highly unlikely, though.
-#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)]
-pub enum EventPriority {
-    /// Work that must be completed during the EventHandler phase.
-    ///
-    /// Currently this is reserved for controlled inputs.
-    Immediate = 3,
-
-    /// "High Priority" work will not interrupt other high priority work, but will interrupt medium and low priority work.
-    ///
-    /// This is typically reserved for things like user interaction.
-    ///
-    /// React calls these "discrete" events, but with an extra category of "user-blocking" (Immediate).
-    High = 2,
-
-    /// "Medium priority" work is generated by page events not triggered by the user. These types of events are less important
-    /// than "High Priority" events and will take precedence over low priority events.
-    ///
-    /// This is typically reserved for VirtualEvents that are not related to keyboard or mouse input.
-    ///
-    /// React calls these "continuous" events (e.g. mouse move, mouse wheel, touch move, etc).
-    Medium = 1,
-
-    /// "Low Priority" work will always be preempted unless the work is significantly delayed, in which case it will be
-    /// advanced to the front of the work queue until completed.
-    ///
-    /// The primary user of Low Priority work is the asynchronous work system (Suspense).
-    ///
-    /// This is considered "idle" work or "background" work.
-    Low = 0,
-}
-
 type ExternalListenerCallback<'bump, T> = BumpBox<'bump, dyn FnMut(T) + 'bump>;
 
 /// The callback type generated by the `rsx!` macro when an `on` field is specified for components.

+ 3 - 3
packages/core/src/factory.rs

@@ -82,9 +82,9 @@ impl ScopeState {
         // let as_dyn: &dyn AnyProps = self.bump().alloc(vcomp);
         // todo: clean up borrowed props
         // if !P::IS_STATIC {
-        //     let vcomp = &*vcomp;
-        //     let vcomp = unsafe { std::mem::transmute(vcomp) };
-        //     self.scope.items.borrow_mut().borrowed_props.push(vcomp);
+        // let vcomp = ex;
+        // let vcomp = unsafe { std::mem::transmute(vcomp) };
+        // self.items.borrow_mut().borrowed_props.push(vcomp);
         // }
 
         DynamicNode::Component(VComponent {

+ 12 - 12
packages/core/src/fragment.rs

@@ -26,17 +26,17 @@ use crate::innerlude::*;
 ///
 /// You want to use this free-function when your fragment needs a key and simply returning multiple nodes from rsx! won't cut it.
 #[allow(non_upper_case_globals, non_snake_case)]
-pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element {
-    let children = cx.props.0.as_ref().unwrap();
-    Ok(VNode {
-        node_id: children.node_id.clone(),
-        key: children.key.clone(),
-        parent: children.parent.clone(),
-        template: children.template,
-        root_ids: children.root_ids,
-        dynamic_nodes: children.dynamic_nodes,
-        dynamic_attrs: children.dynamic_attrs,
-    })
+pub fn Fragment<'a>(_cx: Scope<'a, FragmentProps<'a>>) -> Element {
+    // let children = cx.props.0.as_ref().unwrap();
+    todo!()
+    // Ok(VNode {
+    //     key: children.key.clone(),
+    //     parent: children.parent.clone(),
+    //     template: children.template,
+    //     root_ids: children.root_ids,
+    //     dynamic_nodes: children.dynamic_nodes,
+    //     dynamic_attrs: children.dynamic_attrs,
+    // })
 }
 
 pub struct FragmentProps<'a>(Element<'a>);
@@ -96,7 +96,7 @@ impl<'a> Properties for FragmentProps<'a> {
     type Builder = FragmentBuilder<'a, false>;
     const IS_STATIC: bool = false;
     fn builder() -> Self::Builder {
-        FragmentBuilder(VNode::empty())
+        todo!()
     }
     unsafe fn memoize(&self, _other: &Self) -> bool {
         false

+ 0 - 63
packages/core/src/future_container.rs

@@ -1,63 +0,0 @@
-use futures_channel::mpsc::UnboundedSender;
-use slab::Slab;
-use std::future::Future;
-use std::{cell::RefCell, rc::Rc, sync::Arc};
-
-use crate::innerlude::ScopeId;
-/// The type of message that can be sent to the scheduler.
-///
-/// These messages control how the scheduler will process updates to the UI.
-#[derive(Debug)]
-pub enum SchedulerMsg {
-    /// Events from athe Renderer
-    Event,
-
-    /// Immediate updates from Components that mark them as dirty
-    Immediate(ScopeId),
-
-    /// Mark all components as dirty and update them
-    DirtyAll,
-
-    /// New tasks from components that should be polled when the next poll is ready
-    NewTask(ScopeId),
-}
-
-// todo extract this so spawning doesn't require refcell and rc doesnt need to be tracked
-#[derive(Clone)]
-pub struct FutureQueue {
-    pub sender: UnboundedSender<SchedulerMsg>,
-    pub queue: RefCell<Slab<Arc<dyn Future<Output = ()>>>>,
-}
-
-impl FutureQueue {
-    pub fn new(sender: UnboundedSender<SchedulerMsg>) -> Self {
-        Self {
-            sender,
-            queue: Default::default(),
-        }
-    }
-
-    pub fn spawn(&self, scope: ScopeId, fut: impl Future<Output = ()> + 'static) -> TaskId {
-        let id = self.queue.borrow_mut().insert(Arc::new(fut));
-
-        TaskId { id, scope }
-    }
-
-    pub fn remove(&self, id: TaskId) {
-        todo!()
-    }
-}
-
-/// A task's unique identifier.
-///
-/// `TaskId` is a `usize` that is unique across the entire [`VirtualDom`] and across time. [`TaskID`]s will never be reused
-/// once a Task has been completed.
-#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
-#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
-pub struct TaskId {
-    /// The global ID of the task
-    pub id: usize,
-
-    /// The original scope that this task was scheduled in
-    pub scope: ScopeId,
-}

+ 4 - 5
packages/core/src/lib.rs

@@ -20,6 +20,7 @@ mod virtual_dom;
 pub(crate) mod innerlude {
     pub use crate::arena::*;
     pub use crate::dirty_scope::*;
+    pub use crate::error_boundary::*;
     pub use crate::events::*;
     pub use crate::fragment::*;
     pub use crate::lazynodes::*;
@@ -78,8 +79,6 @@ pub use crate::innerlude::{
     DynamicNode,
     Element,
     ElementId,
-    ElementRef,
-    EventPriority,
     Fragment,
     LazyNodes,
     Mutation,
@@ -109,9 +108,9 @@ pub use crate::innerlude::{
 /// This includes types like [`Scope`], [`Element`], and [`Component`].
 pub mod prelude {
     pub use crate::innerlude::{
-        fc_to_builder, Element, EventHandler, EventPriority, Fragment, LazyNodes, NodeFactory,
-        Properties, Scope, ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute,
-        TemplateNode, UiEvent, VNode, VirtualDom,
+        fc_to_builder, Element, EventHandler, Fragment, LazyNodes, NodeFactory, Properties, Scope,
+        ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, UiEvent,
+        VNode, VirtualDom,
     };
 }
 

+ 1 - 0
packages/core/src/mutations.rs

@@ -1,6 +1,7 @@
 use crate::{arena::ElementId, ScopeId};
 
 #[derive(Debug)]
+#[must_use = "not handling edits can lead to visual inconsistencies in UI"]
 pub struct Mutations<'a> {
     pub subtree: usize,
     pub template_mutations: Vec<Mutation<'a>>,

+ 12 - 13
packages/core/src/nodes.rs

@@ -1,4 +1,5 @@
 use crate::{any_props::AnyProps, arena::ElementId, Element, ScopeId, ScopeState, UiEvent};
+use bumpalo::boxed::Box as BumpBox;
 use std::{
     any::{Any, TypeId},
     cell::{Cell, RefCell},
@@ -12,9 +13,6 @@ pub type TemplateId = &'static str;
 /// static parts of the template.
 #[derive(Debug, Clone)]
 pub struct VNode<'a> {
-    // The ID assigned for the root of this template
-    pub node_id: Cell<ElementId>,
-
     /// The key given to the root of this template.
     ///
     /// In fragments, this is the key of the first child. In other cases, it is the key of the root.
@@ -40,7 +38,6 @@ pub struct VNode<'a> {
 impl<'a> VNode<'a> {
     pub fn empty() -> Element<'a> {
         Ok(VNode {
-            node_id: Cell::new(ElementId(0)),
             key: None,
             parent: None,
             root_ids: &[],
@@ -156,26 +153,28 @@ pub enum AttributeValue<'a> {
     Float(f32),
     Int(i32),
     Bool(bool),
-    Listener(RefCell<&'a mut dyn FnMut(UiEvent<dyn Any>)>),
-    Any(&'a dyn AnyValue),
+    Listener(RefCell<Option<BumpBox<'a, dyn FnMut(UiEvent<dyn Any>) + 'a>>>),
+    Any(BumpBox<'a, dyn AnyValue>),
     None,
 }
 
 impl<'a> AttributeValue<'a> {
     pub fn new_listener<T: 'static>(
         cx: &'a ScopeState,
-        mut f: impl FnMut(UiEvent<T>) + 'a,
+        mut callback: impl FnMut(UiEvent<T>) + 'a,
     ) -> AttributeValue<'a> {
-        AttributeValue::Listener(RefCell::new(cx.bump().alloc(
-            move |event: UiEvent<dyn Any>| {
+        let boxed: BumpBox<'a, dyn FnMut(_) + 'a> = unsafe {
+            BumpBox::from_raw(cx.bump().alloc(move |event: UiEvent<dyn Any>| {
                 if let Ok(data) = event.data.downcast::<T>() {
-                    f(UiEvent {
+                    callback(UiEvent {
                         bubbles: event.bubbles,
                         data,
                     })
                 }
-            },
-        )))
+            }))
+        };
+
+        AttributeValue::Listener(RefCell::new(Some(boxed)))
     }
 }
 
@@ -201,7 +200,7 @@ impl<'a> PartialEq for AttributeValue<'a> {
             (Self::Int(l0), Self::Int(r0)) => l0 == r0,
             (Self::Bool(l0), Self::Bool(r0)) => l0 == r0,
             (Self::Listener(_), Self::Listener(_)) => true,
-            (Self::Any(l0), Self::Any(r0)) => l0.any_cmp(*r0),
+            (Self::Any(l0), Self::Any(r0)) => l0.any_cmp(r0.as_ref()),
             _ => core::mem::discriminant(self) == core::mem::discriminant(other),
         }
     }

+ 52 - 6
packages/core/src/scope_arena.rs

@@ -7,6 +7,7 @@ use crate::{
     scheduler::RcWake,
     scopes::{ScopeId, ScopeState},
     virtual_dom::VirtualDom,
+    AttributeValue, DynamicNode, VFragment, VNode,
 };
 use futures_util::FutureExt;
 use std::{
@@ -17,13 +18,13 @@ use std::{
 };
 
 impl VirtualDom {
-    pub(super) fn new_scope(&mut self, props: *const dyn AnyProps<'static>) -> &mut ScopeState {
+    pub(super) fn new_scope(&mut self, props: Box<dyn AnyProps<'static>>) -> &mut ScopeState {
         let parent = self.acquire_current_scope_raw();
         let entry = self.scopes.vacant_entry();
         let height = unsafe { parent.map(|f| (*f).height + 1).unwrap_or(0) };
         let id = ScopeId(entry.key());
 
-        entry.insert(ScopeState {
+        entry.insert(Box::new(ScopeState {
             parent,
             id,
             height,
@@ -38,19 +39,64 @@ impl VirtualDom {
             hook_idx: Default::default(),
             shared_contexts: Default::default(),
             tasks: self.scheduler.clone(),
-        })
+        }))
     }
 
     fn acquire_current_scope_raw(&mut self) -> Option<*mut ScopeState> {
         self.scope_stack
             .last()
             .copied()
-            .and_then(|id| self.scopes.get_mut(id.0).map(|f| f as *mut ScopeState))
+            .and_then(|id| self.scopes.get_mut(id.0).map(|f| f.as_mut() as *mut _))
+    }
+
+    pub fn ensure_drop_safety(&self, scope: ScopeId) {
+        let scope = &self.scopes[scope.0];
+        let node = unsafe { scope.previous_frame().try_load_node() };
+
+        // And now we want to make sure the previous frame has dropped anything that borrows self
+        if let Some(RenderReturn::Sync(Ok(node))) = node {
+            self.ensure_drop_safety_inner(node);
+        }
+    }
+
+    fn ensure_drop_safety_inner(&self, node: &VNode) {
+        for attr in node.dynamic_attrs {
+            if let AttributeValue::Listener(l) = &attr.value {
+                l.borrow_mut().take();
+            }
+        }
+
+        for child in node.dynamic_nodes {
+            match child {
+                DynamicNode::Component(c) => {
+                    // Only descend if the props are borrowed
+                    if !c.static_props {
+                        self.ensure_drop_safety(c.scope.get().unwrap());
+                        c.props.set(None);
+                    }
+                }
+                DynamicNode::Fragment(VFragment::NonEmpty(f)) => {
+                    for node in *f {
+                        self.ensure_drop_safety_inner(node);
+                    }
+                }
+                _ => {}
+            }
+        }
     }
 
     pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> &RenderReturn {
+        // Cycle to the next frame and then reset it
+        // This breaks any latent references, invalidating every pointer referencing into it.
+        // Remove all the outdated listeners
+        self.ensure_drop_safety(scope_id);
+
         let mut new_nodes = unsafe {
-            let scope = &mut self.scopes[scope_id.0];
+            let scope = self.scopes[scope_id.0].as_mut();
+
+            scope.previous_frame_mut().bump.reset();
+
+            // Make sure to reset the hook counter so we give out hooks in the right order
             scope.hook_idx.set(0);
 
             // safety: due to how we traverse the tree, we know that the scope is not currently aliased
@@ -113,7 +159,7 @@ impl VirtualDom {
         let frame = scope.previous_frame();
 
         // set the new head of the bump frame
-        let alloced = frame.bump.alloc(new_nodes);
+        let alloced = &*frame.bump.alloc(new_nodes);
         frame.node.set(alloced);
 
         // And move the render generation forward by one

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

@@ -82,12 +82,12 @@ pub struct ScopeState {
     pub(crate) tasks: Rc<Scheduler>,
     pub(crate) spawned_tasks: HashSet<TaskId>,
 
-    pub(crate) props: *const dyn AnyProps<'static>,
+    pub(crate) props: Box<dyn AnyProps<'static>>,
     pub(crate) placeholder: Cell<Option<ElementId>>,
 }
 
 impl ScopeState {
-    pub fn current_frame(&self) -> &BumpFrame {
+    pub(crate) fn current_frame(&self) -> &BumpFrame {
         match self.render_cnt.get() % 2 {
             0 => &self.node_arena_1,
             1 => &self.node_arena_2,
@@ -95,7 +95,7 @@ impl ScopeState {
         }
     }
 
-    pub fn previous_frame(&self) -> &BumpFrame {
+    pub(crate) fn previous_frame(&self) -> &BumpFrame {
         match self.render_cnt.get() % 2 {
             1 => &self.node_arena_1,
             0 => &self.node_arena_2,
@@ -103,6 +103,14 @@ impl ScopeState {
         }
     }
 
+    pub(crate) fn previous_frame_mut(&mut self) -> &mut BumpFrame {
+        match self.render_cnt.get() % 2 {
+            1 => &mut self.node_arena_1,
+            0 => &mut self.node_arena_2,
+            _ => unreachable!(),
+        }
+    }
+
     /// Get the current render since the inception of this component
     ///
     /// This can be used as a helpful diagnostic when debugging hooks/renders, etc
@@ -117,7 +125,8 @@ impl ScopeState {
     ///
     /// If you need to allocate items that need to be dropped, use bumpalo's box.
     pub fn bump(&self) -> &Bump {
-        &self.current_frame().bump
+        // note that this is actually the previous frame since we use that as scratch space while the component is rendering
+        &self.previous_frame().bump
     }
 
     /// Get a handle to the currently active head node arena for this Scope

+ 17 - 17
packages/core/src/virtual_dom.rs

@@ -6,12 +6,12 @@ use crate::{
     any_props::VProps,
     arena::{ElementId, ElementRef},
     factory::RenderReturn,
-    innerlude::{DirtyScope, ElementPath, Mutations, Scheduler, SchedulerMsg},
+    innerlude::{DirtyScope, Mutations, Scheduler, SchedulerMsg, ErrorBoundary},
     mutations::Mutation,
     nodes::{Template, TemplateId},
     scheduler::{SuspenseBoundary, SuspenseId},
     scopes::{ScopeId, ScopeState},
-    AttributeValue, Element, EventPriority, Scope, SuspenseContext, UiEvent,
+    AttributeValue, Element, Scope, SuspenseContext, UiEvent,
 };
 use futures_util::{pin_mut, StreamExt};
 use slab::Slab;
@@ -149,7 +149,7 @@ use std::{
 /// ```
 pub struct VirtualDom {
     pub(crate) templates: HashMap<TemplateId, Template<'static>>,
-    pub(crate) scopes: Slab<ScopeState>,
+    pub(crate) scopes: Slab<Box<ScopeState>>,
     pub(crate) dirty_scopes: BTreeSet<DirtyScope>,
     pub(crate) scheduler: Rc<Scheduler>,
 
@@ -224,10 +224,7 @@ impl VirtualDom {
     /// let mut dom = VirtualDom::new_with_props(Example, SomeProps { name: "jane" });
     /// let mutations = dom.rebuild();
     /// ```
-    pub fn new_with_props<P>(root: fn(Scope<P>) -> Element, root_props: P) -> Self
-    where
-        P: 'static,
-    {
+    pub fn new_with_props<P: 'static>(root: fn(Scope<P>) -> Element, root_props: P) -> Self {
         let (tx, rx) = futures_channel::mpsc::unbounded();
         let mut dom = Self {
             rx,
@@ -242,11 +239,11 @@ impl VirtualDom {
             mutations: Mutations::new(),
         };
 
-        let root = dom.new_scope(Box::leak(Box::new(VProps::new(
+        let root = dom.new_scope(Box::new(VProps::new(
             root,
             |_, _| unreachable!(),
             root_props,
-        ))));
+        )));
 
         // The root component is always a suspense boundary for any async children
         // This could be unexpected, so we might rethink this behavior later
@@ -254,6 +251,9 @@ impl VirtualDom {
         // We *could* just panic if the suspense boundary is not found
         root.provide_context(Rc::new(SuspenseBoundary::new(ScopeId(0))));
 
+        // Unlike react, we provide a default error boundary that just renders the error as a string
+        root.provide_context(Rc::new(ErrorBoundary::new(ScopeId(0))));
+
         // the root element is always given element ID 0 since it's the container for the entire tree
         dom.elements.insert(ElementRef::null());
 
@@ -264,7 +264,7 @@ impl VirtualDom {
     ///
     /// This is useful for inserting or removing contexts from a scope, or rendering out its root node
     pub fn get_scope(&self, id: ScopeId) -> Option<&ScopeState> {
-        self.scopes.get(id.0)
+        self.scopes.get(id.0).map(|f| f.as_ref())
     }
 
     /// Get the single scope at the top of the VirtualDom tree that will always be around
@@ -285,7 +285,7 @@ impl VirtualDom {
     /// Manually mark a scope as requiring a re-render
     ///
     /// Whenever the VirtualDom "works", it will re-render this scope
-    pub fn mark_dirty_scope(&mut self, id: ScopeId) {
+    pub fn mark_dirty(&mut self, id: ScopeId) {
         let height = self.scopes[id.0].height;
         self.dirty_scopes.insert(DirtyScope { height, id });
     }
@@ -329,7 +329,6 @@ impl VirtualDom {
         data: Rc<dyn Any>,
         element: ElementId,
         bubbles: bool,
-        _priority: EventPriority,
     ) {
         /*
         ------------------------
@@ -393,7 +392,10 @@ impl VirtualDom {
             // We check the bubble state between each call to see if the event has been stopped from bubbling
             for listener in listeners.drain(..).rev() {
                 if let AttributeValue::Listener(listener) = listener {
-                    listener.borrow_mut()(uievent.clone());
+                    if let Some(cb) = listener.borrow_mut().as_deref_mut() {
+                        cb(uievent.clone());
+                    }
+
                     if !uievent.bubbles.get() {
                         return;
                     }
@@ -427,7 +429,7 @@ impl VirtualDom {
             match some_msg.take() {
                 // If a bunch of messages are ready in a sequence, try to pop them off synchronously
                 Some(msg) => match msg {
-                    SchedulerMsg::Immediate(id) => self.mark_dirty_scope(id),
+                    SchedulerMsg::Immediate(id) => self.mark_dirty(id),
                     SchedulerMsg::TaskNotified(task) => self.handle_task_wakeup(task),
                     SchedulerMsg::SuspenseNotified(id) => self.handle_suspense_wakeup(id),
                 },
@@ -479,7 +481,6 @@ impl VirtualDom {
     ///
     /// apply_edits(edits);
     /// ```
-    #[must_use]
     pub fn rebuild<'a>(&'a mut self) -> Mutations<'a> {
         match unsafe { self.run_scope(ScopeId(0)).extend_lifetime_ref() } {
             // Rebuilding implies we append the created elements to the root
@@ -497,7 +498,6 @@ impl VirtualDom {
 
     /// Render whatever the VirtualDom has ready as fast as possible without requiring an executor to progress
     /// suspended subtrees.
-    #[must_use]
     pub fn render_immediate(&mut self) -> Mutations {
         // Build a waker that won't wake up since our deadline is already expired when it's polled
         let waker = futures_util::task::noop_waker();
@@ -612,6 +612,6 @@ impl VirtualDom {
 
 impl Drop for VirtualDom {
     fn drop(&mut self) {
-        // self.drop_scope(ScopeId(0));
+        self.drop_scope(ScopeId(0));
     }
 }

+ 4 - 4
packages/core/tests/attr_cleanup.rs

@@ -30,7 +30,7 @@ fn attrs_cycle() {
         ]
     );
 
-    dom.mark_dirty_scope(ScopeId(0));
+    dom.mark_dirty(ScopeId(0));
     assert_eq!(
         dom.render_immediate().santize().edits,
         [
@@ -42,7 +42,7 @@ fn attrs_cycle() {
         ]
     );
 
-    dom.mark_dirty_scope(ScopeId(0));
+    dom.mark_dirty(ScopeId(0));
     assert_eq!(
         dom.render_immediate().santize().edits,
         [
@@ -51,7 +51,7 @@ fn attrs_cycle() {
         ]
     );
 
-    dom.mark_dirty_scope(ScopeId(0));
+    dom.mark_dirty(ScopeId(0));
     assert_eq!(
         dom.render_immediate().santize().edits,
         [
@@ -64,7 +64,7 @@ fn attrs_cycle() {
     );
 
     // we take the node taken by attributes since we reused it
-    dom.mark_dirty_scope(ScopeId(0));
+    dom.mark_dirty(ScopeId(0));
     assert_eq!(
         dom.render_immediate().santize().edits,
         [

+ 1 - 3
packages/core/tests/bubble_error.rs

@@ -22,9 +22,7 @@ fn it_goes() {
 
     let edits = dom.rebuild().santize();
 
-    dbg!(edits);
-
-    dom.mark_dirty_scope(ScopeId(0));
+    dom.mark_dirty(ScopeId(0));
 
     dom.render_immediate();
 }

+ 7 - 7
packages/core/tests/context_api.rs

@@ -27,22 +27,22 @@ fn state_shares() {
         ]
     );
 
-    dom.mark_dirty_scope(ScopeId(0));
-    dom.render_immediate();
+    dom.mark_dirty(ScopeId(0));
+    _ = dom.render_immediate();
     assert_eq!(dom.base_scope().consume_context::<i32>().unwrap(), 1);
 
-    dom.mark_dirty_scope(ScopeId(0));
-    dom.render_immediate();
+    dom.mark_dirty(ScopeId(0));
+    _ = dom.render_immediate();
     assert_eq!(dom.base_scope().consume_context::<i32>().unwrap(), 2);
 
-    dom.mark_dirty_scope(ScopeId(2));
+    dom.mark_dirty(ScopeId(2));
     assert_eq!(
         dom.render_immediate().santize().edits,
         [SetText { value: "Value is 2", id: ElementId(1,) },]
     );
 
-    dom.mark_dirty_scope(ScopeId(0));
-    dom.mark_dirty_scope(ScopeId(2));
+    dom.mark_dirty(ScopeId(0));
+    dom.mark_dirty(ScopeId(2));
     let edits = dom.render_immediate();
     assert_eq!(
         edits.santize().edits,

+ 1 - 1
packages/core/tests/create_lists.rs

@@ -43,7 +43,7 @@ fn list_renders() {
             CreateStaticText { value: "hello world! " },
             AppendChildren { m: 1 },
             CreateElement { name: "p" },
-            CreateStaticText { value: "d" },
+            CreateTextPlaceholder,
             AppendChildren { m: 1 },
             AppendChildren { m: 2 },
             SaveTemplate { name: "template", m: 1 }

+ 3 - 3
packages/core/tests/cycle.rs

@@ -21,7 +21,7 @@ fn cycling_elements() {
         ]
     );
 
-    dom.mark_dirty_scope(ScopeId(0));
+    dom.mark_dirty(ScopeId(0));
     assert_eq!(
         dom.render_immediate().santize().edits,
         [
@@ -31,7 +31,7 @@ fn cycling_elements() {
     );
 
     // notice that the IDs cycle back to ElementId(1), preserving a minimal memory footprint
-    dom.mark_dirty_scope(ScopeId(0));
+    dom.mark_dirty(ScopeId(0));
     assert_eq!(
         dom.render_immediate().santize().edits,
         [
@@ -40,7 +40,7 @@ fn cycling_elements() {
         ]
     );
 
-    dom.mark_dirty_scope(ScopeId(0));
+    dom.mark_dirty(ScopeId(0));
     assert_eq!(
         dom.render_immediate().santize().edits,
         [

+ 102 - 0
packages/core/tests/diff_component.rs

@@ -0,0 +1,102 @@
+use dioxus::core::{ElementId, Mutation::*};
+use dioxus::prelude::*;
+
+/// When returning sets of components, we do a light diff of the contents to preserve some react-like functionality
+///
+/// This means that nav_bar should never get re-created and that we should only be swapping out
+/// different pointers
+#[test]
+fn component_swap() {
+    fn app(cx: Scope) -> Element {
+        let render_phase = cx.use_hook(|| 0);
+
+        *render_phase += 1;
+
+        cx.render(match *render_phase {
+            0 => rsx! {
+                nav_bar {}
+                dash_board {}
+            },
+            1 => rsx! {
+                nav_bar {}
+                dash_results {}
+            },
+            2 => rsx! {
+                nav_bar {}
+                dash_board {}
+            },
+            3 => rsx! {
+                nav_bar {}
+                dash_results {}
+            },
+            4 => rsx! {
+                nav_bar {}
+                dash_board {}
+            },
+            _ => rsx!("blah"),
+        })
+    }
+
+    fn nav_bar(cx: Scope) -> Element {
+        cx.render(rsx! {
+            h1 {
+                "NavBar"
+                (0..3).map(|_| rsx!(nav_link {}))
+            }
+        })
+    }
+
+    fn nav_link(cx: Scope) -> Element {
+        cx.render(rsx!( h1 { "nav_link" } ))
+    }
+
+    fn dash_board(cx: Scope) -> Element {
+        cx.render(rsx!( div { "dashboard" } ))
+    }
+
+    fn dash_results(cx: Scope) -> Element {
+        cx.render(rsx!( div { "results" } ))
+    }
+
+    let mut dom = VirtualDom::new(app);
+    let edits = dom.rebuild().santize();
+    assert_eq!(
+        edits.edits,
+        [
+            LoadTemplate { name: "template", index: 0, id: ElementId(1) },
+            LoadTemplate { name: "template", index: 0, id: ElementId(2) },
+            LoadTemplate { name: "template", index: 0, id: ElementId(3) },
+            LoadTemplate { name: "template", index: 0, id: ElementId(4) },
+            ReplacePlaceholder { path: &[1], m: 3 },
+            LoadTemplate { name: "template", index: 0, id: ElementId(5) },
+            AppendChildren { m: 2 }
+        ]
+    );
+
+    dom.mark_dirty(ScopeId(0));
+    assert_eq!(
+        dom.render_immediate().santize().edits,
+        [
+            LoadTemplate { name: "template", index: 0, id: ElementId(6) },
+            ReplaceWith { id: ElementId(5), m: 1 }
+        ]
+    );
+
+    dom.mark_dirty(ScopeId(0));
+    assert_eq!(
+        dom.render_immediate().santize().edits,
+        [
+            LoadTemplate { name: "template", index: 0, id: ElementId(5) },
+            ReplaceWith { id: ElementId(6), m: 1 }
+        ]
+    );
+
+    dom.mark_dirty(ScopeId(0));
+    assert_eq!(
+        dom.render_immediate().santize().edits,
+        [
+            LoadTemplate { name: "template", index: 0, id: ElementId(6) },
+            ReplaceWith { id: ElementId(5), m: 1 }
+        ]
+    );
+}

+ 9 - 9
packages/core/tests/diff_element.rs

@@ -10,21 +10,21 @@ fn text_diff() {
     }
 
     let mut vdom = VirtualDom::new(app);
-    vdom.rebuild();
+    _ = vdom.rebuild();
 
-    vdom.mark_dirty_scope(ScopeId(0));
+    vdom.mark_dirty(ScopeId(0));
     assert_eq!(
         vdom.render_immediate().edits,
         [SetText { value: "hello 1", id: ElementId(2) }]
     );
 
-    vdom.mark_dirty_scope(ScopeId(0));
+    vdom.mark_dirty(ScopeId(0));
     assert_eq!(
         vdom.render_immediate().edits,
         [SetText { value: "hello 2", id: ElementId(2) }]
     );
 
-    vdom.mark_dirty_scope(ScopeId(0));
+    vdom.mark_dirty(ScopeId(0));
     assert_eq!(
         vdom.render_immediate().edits,
         [SetText { value: "hello 3", id: ElementId(2) }]
@@ -44,9 +44,9 @@ fn element_swap() {
     }
 
     let mut vdom = VirtualDom::new(app);
-    vdom.rebuild();
+    _ = vdom.rebuild();
 
-    vdom.mark_dirty_scope(ScopeId(0));
+    vdom.mark_dirty(ScopeId(0));
     assert_eq!(
         vdom.render_immediate().santize().edits,
         [
@@ -55,7 +55,7 @@ fn element_swap() {
         ]
     );
 
-    vdom.mark_dirty_scope(ScopeId(0));
+    vdom.mark_dirty(ScopeId(0));
     assert_eq!(
         vdom.render_immediate().santize().edits,
         [
@@ -64,7 +64,7 @@ fn element_swap() {
         ]
     );
 
-    vdom.mark_dirty_scope(ScopeId(0));
+    vdom.mark_dirty(ScopeId(0));
     assert_eq!(
         vdom.render_immediate().santize().edits,
         [
@@ -73,7 +73,7 @@ fn element_swap() {
         ]
     );
 
-    vdom.mark_dirty_scope(ScopeId(0));
+    vdom.mark_dirty(ScopeId(0));
     assert_eq!(
         vdom.render_immediate().santize().edits,
         [

+ 39 - 13
packages/core/tests/diff_keyed_list.rs

@@ -1,5 +1,3 @@
-#![allow(unused, non_upper_case_globals)]
-
 //! Diffing Tests
 //!
 //! These tests only verify that the diffing algorithm works properly for single components.
@@ -39,7 +37,7 @@ fn keyed_diffing_out_of_order() {
         ]
     );
 
-    dom.mark_dirty_scope(ScopeId(0));
+    dom.mark_dirty(ScopeId(0));
     assert_eq!(
         dom.render_immediate().edits,
         [
@@ -64,7 +62,7 @@ fn keyed_diffing_out_of_order_adds() {
 
     _ = dom.rebuild();
 
-    dom.mark_dirty_scope(ScopeId(0));
+    dom.mark_dirty(ScopeId(0));
     assert_eq!(
         dom.render_immediate().edits,
         [
@@ -90,7 +88,7 @@ fn keyed_diffing_out_of_order_adds_3() {
 
     _ = dom.rebuild();
 
-    dom.mark_dirty_scope(ScopeId(0));
+    dom.mark_dirty(ScopeId(0));
     assert_eq!(
         dom.render_immediate().edits,
         [
@@ -116,7 +114,7 @@ fn keyed_diffing_out_of_order_adds_4() {
 
     _ = dom.rebuild();
 
-    dom.mark_dirty_scope(ScopeId(0));
+    dom.mark_dirty(ScopeId(0));
     assert_eq!(
         dom.render_immediate().edits,
         [
@@ -142,7 +140,7 @@ fn keyed_diffing_out_of_order_adds_5() {
 
     _ = dom.rebuild();
 
-    dom.mark_dirty_scope(ScopeId(0));
+    dom.mark_dirty(ScopeId(0));
     assert_eq!(
         dom.render_immediate().edits,
         [
@@ -167,7 +165,7 @@ fn keyed_diffing_additions() {
 
     _ = dom.rebuild();
 
-    dom.mark_dirty_scope(ScopeId(0));
+    dom.mark_dirty(ScopeId(0));
     assert_eq!(
         dom.render_immediate().santize().edits,
         [
@@ -192,7 +190,7 @@ fn keyed_diffing_additions_and_moves_on_ends() {
 
     _ = dom.rebuild();
 
-    dom.mark_dirty_scope(ScopeId(0));
+    dom.mark_dirty(ScopeId(0));
     assert_eq!(
         dom.render_immediate().santize().edits,
         [
@@ -222,7 +220,7 @@ fn keyed_diffing_additions_and_moves_in_middle() {
     _ = dom.rebuild();
 
     // LIS: 4, 5, 6
-    dom.mark_dirty_scope(ScopeId(0));
+    dom.mark_dirty(ScopeId(0));
     assert_eq!(
         dom.render_immediate().santize().edits,
         [
@@ -256,7 +254,7 @@ fn controlled_keyed_diffing_out_of_order() {
     _ = dom.rebuild();
 
     // LIS: 5, 6
-    dom.mark_dirty_scope(ScopeId(0));
+    dom.mark_dirty(ScopeId(0));
     assert_eq!(
         dom.render_immediate().santize().edits,
         [
@@ -289,7 +287,7 @@ fn controlled_keyed_diffing_out_of_order_max_test() {
 
     _ = dom.rebuild();
 
-    dom.mark_dirty_scope(ScopeId(0));
+    dom.mark_dirty(ScopeId(0));
     assert_eq!(
         dom.render_immediate().santize().edits,
         [
@@ -318,7 +316,7 @@ fn remove_list() {
 
     _ = dom.rebuild();
 
-    dom.mark_dirty_scope(ScopeId(0));
+    dom.mark_dirty(ScopeId(0));
     assert_eq!(
         dom.render_immediate().santize().edits,
         [
@@ -328,3 +326,31 @@ fn remove_list() {
         ]
     );
 }
+
+#[test]
+fn no_common_keys() {
+    let mut dom = VirtualDom::new(|cx| {
+        let order: &[_] = match cx.generation() % 2 {
+            0 => &[1, 2, 3],
+            1 => &[4, 5, 6],
+            _ => unreachable!(),
+        };
+
+        cx.render(rsx!(order.iter().map(|i| rsx!(div { key: "{i}" }))))
+    });
+
+    _ = dom.rebuild();
+
+    dom.mark_dirty(ScopeId(0));
+    assert_eq!(
+        dom.render_immediate().santize().edits,
+        [
+            Remove { id: ElementId(2) },
+            Remove { id: ElementId(3) },
+            LoadTemplate { name: "template", index: 0, id: ElementId(3) },
+            LoadTemplate { name: "template", index: 0, id: ElementId(2) },
+            LoadTemplate { name: "template", index: 0, id: ElementId(4) },
+            ReplaceWith { id: ElementId(1), m: 3 }
+        ]
+    );
+}

+ 18 - 18
packages/core/tests/diff_unkeyed_list.rs

@@ -26,7 +26,7 @@ fn list_creates_one_by_one() {
     );
 
     // Rendering the first item should replace the placeholder with an element
-    dom.mark_dirty_scope(ScopeId(0));
+    dom.mark_dirty(ScopeId(0));
     assert_eq!(
         dom.render_immediate().santize().edits,
         [
@@ -37,7 +37,7 @@ fn list_creates_one_by_one() {
     );
 
     // Rendering the next item should insert after the previous
-    dom.mark_dirty_scope(ScopeId(0));
+    dom.mark_dirty(ScopeId(0));
     assert_eq!(
         dom.render_immediate().santize().edits,
         [
@@ -48,7 +48,7 @@ fn list_creates_one_by_one() {
     );
 
     // ... and again!
-    dom.mark_dirty_scope(ScopeId(0));
+    dom.mark_dirty(ScopeId(0));
     assert_eq!(
         dom.render_immediate().santize().edits,
         [
@@ -59,7 +59,7 @@ fn list_creates_one_by_one() {
     );
 
     // once more
-    dom.mark_dirty_scope(ScopeId(0));
+    dom.mark_dirty(ScopeId(0));
     assert_eq!(
         dom.render_immediate().santize().edits,
         [
@@ -106,14 +106,14 @@ fn removes_one_by_one() {
 
     // Remove div(3)
     // Rendering the first item should replace the placeholder with an element
-    dom.mark_dirty_scope(ScopeId(0));
+    dom.mark_dirty(ScopeId(0));
     assert_eq!(
         dom.render_immediate().santize().edits,
         [Remove { id: ElementId(6) }]
     );
 
     // Remove div(2)
-    dom.mark_dirty_scope(ScopeId(0));
+    dom.mark_dirty(ScopeId(0));
     assert_eq!(
         dom.render_immediate().santize().edits,
         [Remove { id: ElementId(4) }]
@@ -121,7 +121,7 @@ fn removes_one_by_one() {
 
     // Remove div(1) and replace with a placeholder
     // todo: this should just be a remove with no placeholder
-    dom.mark_dirty_scope(ScopeId(0));
+    dom.mark_dirty(ScopeId(0));
     assert_eq!(
         dom.render_immediate().santize().edits,
         [
@@ -132,7 +132,7 @@ fn removes_one_by_one() {
 
     // load the 3 and replace the placeholder
     // todo: this should actually be append to, but replace placeholder is fine for now
-    dom.mark_dirty_scope(ScopeId(0));
+    dom.mark_dirty(ScopeId(0));
     assert_eq!(
         dom.render_immediate().santize().edits,
         [
@@ -169,7 +169,7 @@ fn list_shrink_multiroot() {
         ]
     );
 
-    dom.mark_dirty_scope(ScopeId(0));
+    dom.mark_dirty(ScopeId(0));
     assert_eq!(
         dom.render_immediate().santize().edits,
         [
@@ -181,7 +181,7 @@ fn list_shrink_multiroot() {
         ]
     );
 
-    dom.mark_dirty_scope(ScopeId(0));
+    dom.mark_dirty(ScopeId(0));
     assert_eq!(
         dom.render_immediate().santize().edits,
         [
@@ -193,7 +193,7 @@ fn list_shrink_multiroot() {
         ]
     );
 
-    dom.mark_dirty_scope(ScopeId(0));
+    dom.mark_dirty(ScopeId(0));
     assert_eq!(
         dom.render_immediate().santize().edits,
         [
@@ -248,19 +248,19 @@ fn removes_one_by_one_multiroot() {
         ]
     );
 
-    dom.mark_dirty_scope(ScopeId(0));
+    dom.mark_dirty(ScopeId(0));
     assert_eq!(
         dom.render_immediate().santize().edits,
         [Remove { id: ElementId(10) }, Remove { id: ElementId(12) }]
     );
 
-    dom.mark_dirty_scope(ScopeId(0));
+    dom.mark_dirty(ScopeId(0));
     assert_eq!(
         dom.render_immediate().santize().edits,
         [Remove { id: ElementId(6) }, Remove { id: ElementId(8) }]
     );
 
-    dom.mark_dirty_scope(ScopeId(0));
+    dom.mark_dirty(ScopeId(0));
     assert_eq!(
         dom.render_immediate().santize().edits,
         [
@@ -324,7 +324,7 @@ fn remove_many() {
         ]
     );
 
-    dom.mark_dirty_scope(ScopeId(0));
+    dom.mark_dirty(ScopeId(0));
     let edits = dom.render_immediate().santize();
     assert_eq!(
         edits.edits,
@@ -335,7 +335,7 @@ fn remove_many() {
         ]
     );
 
-    dom.mark_dirty_scope(ScopeId(0));
+    dom.mark_dirty(ScopeId(0));
     let edits = dom.render_immediate().santize();
     assert_eq!(
         edits.edits,
@@ -352,7 +352,7 @@ fn remove_many() {
         ]
     );
 
-    dom.mark_dirty_scope(ScopeId(0));
+    dom.mark_dirty(ScopeId(0));
     let edits = dom.render_immediate().santize();
     assert_eq!(
         edits.edits,
@@ -366,7 +366,7 @@ fn remove_many() {
         ]
     );
 
-    dom.mark_dirty_scope(ScopeId(0));
+    dom.mark_dirty(ScopeId(0));
     let edits = dom.render_immediate().santize();
     assert_eq!(
         edits.edits,

+ 1 - 0
packages/core/tests/hotreloading.rs

@@ -0,0 +1 @@
+//! It should be possible to swap out templates at runtime, enabling hotreloading

+ 3 - 104
packages/core/tests/lifecycle.rs

@@ -44,10 +44,7 @@ fn events_generate() {
 
         match *count {
             0 => cx.render(rsx! {
-                div {
-                    onclick: move |_| {
-                        *count += 1
-                    },
+                div { onclick: move |_| *count += 1,
                     div { "nested" }
                     "Click me!"
                 }
@@ -59,15 +56,9 @@ fn events_generate() {
     let mut dom = VirtualDom::new(app);
     _ = dom.rebuild();
 
-    dom.handle_event(
-        "click",
-        Rc::new(MouseData::default()),
-        ElementId(1),
-        true,
-        EventPriority::Immediate,
-    );
+    dom.handle_event("click", Rc::new(MouseData::default()), ElementId(1), true);
 
-    dom.mark_dirty_scope(ScopeId(0));
+    dom.mark_dirty(ScopeId(0));
     let edits = dom.render_immediate();
 
     assert_eq!(
@@ -175,95 +166,3 @@ fn events_generate() {
 //         ]
 //     );
 // }
-
-// #[test]
-// fn component_swap() {
-//     fn app(cx: Scope) -> Element {
-//         let render_phase = cx.use_hook(|| 0);
-//         *render_phase += 1;
-
-//         cx.render(match *render_phase {
-//             0 => rsx_without_templates!(
-//                 div {
-//                     NavBar {}
-//                     Dashboard {}
-//                 }
-//             ),
-//             1 => rsx_without_templates!(
-//                 div {
-//                     NavBar {}
-//                     Results {}
-//                 }
-//             ),
-//             2 => rsx_without_templates!(
-//                 div {
-//                     NavBar {}
-//                     Dashboard {}
-//                 }
-//             ),
-//             3 => rsx_without_templates!(
-//                 div {
-//                     NavBar {}
-//                     Results {}
-//                 }
-//             ),
-//             4 => rsx_without_templates!(
-//                 div {
-//                     NavBar {}
-//                     Dashboard {}
-//                 }
-//             ),
-//             _ => rsx_without_templates!("blah"),
-//         })
-//     };
-
-//     static NavBar: Component = |cx| {
-//         println!("running navbar");
-//         cx.render(rsx_without_templates! {
-//             h1 {
-//                 "NavBar"
-//                 {(0..3).map(|f| rsx_without_templates!(NavLink {}))}
-//             }
-//         })
-//     };
-
-//     static NavLink: Component = |cx| {
-//         println!("running navlink");
-//         cx.render(rsx_without_templates! {
-//             h1 {
-//                 "NavLink"
-//             }
-//         })
-//     };
-
-//     static Dashboard: Component = |cx| {
-//         println!("running dashboard");
-//         cx.render(rsx_without_templates! {
-//             div {
-//                 "dashboard"
-//             }
-//         })
-//     };
-
-//     static Results: Component = |cx| {
-//         println!("running results");
-//         cx.render(rsx_without_templates! {
-//             div {
-//                 "results"
-//             }
-//         })
-//     };
-
-//     let mut dom = VirtualDom::new(app);
-//     let edits = dom.rebuild();
-//     dbg!(&edits);
-
-//     let edits = dom.work_with_deadline(|| false);
-//     dbg!(&edits);
-//     let edits = dom.work_with_deadline(|| false);
-//     dbg!(&edits);
-//     let edits = dom.work_with_deadline(|| false);
-//     dbg!(&edits);
-//     let edits = dom.work_with_deadline(|| false);
-//     dbg!(&edits);
-// }

+ 127 - 0
packages/core/tests/miri_simple.rs

@@ -0,0 +1,127 @@
+use dioxus::prelude::*;
+
+#[test]
+fn app_drops() {
+    fn app(cx: Scope) -> Element {
+        cx.render(rsx! {
+            div {}
+        })
+    }
+
+    let mut dom = VirtualDom::new(app);
+
+    _ = dom.rebuild();
+    dom.mark_dirty(ScopeId(0));
+    _ = dom.render_immediate();
+}
+
+#[test]
+fn hooks_drop() {
+    fn app(cx: Scope) -> Element {
+        cx.use_hook(|| String::from("asd"));
+        cx.use_hook(|| String::from("asd"));
+        cx.use_hook(|| String::from("asd"));
+        cx.use_hook(|| String::from("asd"));
+
+        cx.render(rsx! {
+            div {}
+        })
+    }
+
+    let mut dom = VirtualDom::new(app);
+
+    _ = dom.rebuild();
+    dom.mark_dirty(ScopeId(0));
+    _ = dom.render_immediate();
+}
+
+#[test]
+fn contexts_drop() {
+    fn app(cx: Scope) -> Element {
+        cx.provide_context(String::from("asd"));
+
+        cx.render(rsx! {
+            div {
+                child_comp {}
+            }
+        })
+    }
+
+    fn child_comp(cx: Scope) -> Element {
+        let el = cx.consume_context::<String>().unwrap();
+
+        cx.render(rsx! {
+            div { "hello {el}" }
+        })
+    }
+
+    let mut dom = VirtualDom::new(app);
+
+    _ = dom.rebuild();
+    dom.mark_dirty(ScopeId(0));
+    _ = dom.render_immediate();
+}
+
+#[test]
+fn tasks_drop() {
+    fn app(cx: Scope) -> Element {
+        cx.spawn(async {
+            tokio::time::sleep(std::time::Duration::from_millis(100000)).await;
+        });
+
+        cx.render(rsx! {
+            div { }
+        })
+    }
+
+    let mut dom = VirtualDom::new(app);
+
+    _ = dom.rebuild();
+    dom.mark_dirty(ScopeId(0));
+    _ = dom.render_immediate();
+}
+
+#[test]
+fn root_props_drop() {
+    struct RootProps(String);
+
+    let mut dom = VirtualDom::new_with_props(
+        |cx| cx.render(rsx!( div { "{cx.props.0}"  } )),
+        RootProps("asdasd".to_string()),
+    );
+
+    _ = dom.rebuild();
+    dom.mark_dirty(ScopeId(0));
+    _ = dom.render_immediate();
+}
+
+#[test]
+fn diffing_drops_old() {
+    fn app(cx: Scope) -> Element {
+        cx.render(rsx! {
+            div {
+                match cx.generation() % 2 {
+                    0 => rsx!( child_comp1 { name: "asdasd".to_string() }),
+                    1 => rsx!( child_comp2 { name: "asdasd".to_string() }),
+                    _ => todo!()
+                }
+            }
+        })
+    }
+
+    #[inline_props]
+    fn child_comp1(cx: Scope, name: String) -> Element {
+        cx.render(rsx! { "Hello {name}" })
+    }
+
+    #[inline_props]
+    fn child_comp2(cx: Scope, name: String) -> Element {
+        cx.render(rsx! { "Goodbye {name}"  })
+    }
+
+    let mut dom = VirtualDom::new(app);
+    _ = dom.rebuild();
+    dom.mark_dirty(ScopeId(0));
+
+    _ = dom.render_immediate();
+}

+ 267 - 324
packages/core/tests/miri_stress.rs

@@ -1,32 +1,28 @@
 #![allow(non_snake_case)]
-/*
-Stress Miri as much as possible.
 
-Prove that we don't leak memory and that our methods are safe.
-
-Specifically:
-- [ ] VirtualDom drops memory safely
-- [ ] Borrowed components don't expose invalid pointers
-- [ ] Async isn't busted
-*/
+use std::rc::Rc;
 
 use dioxus::prelude::*;
 
-/// This test ensures that if a component aborts early, it is replaced with a placeholder.
-/// In debug, this should also toss a warning.
+/// This test checks that we should release all memory used by the virtualdom when it exits.
+///
+/// When miri runs, it'll let us know if we leaked or aliased.
 #[test]
-#[ignore]
 fn test_memory_leak() {
     fn app(cx: Scope) -> Element {
-        let val = cx.use_hook(|| 0);
+        let val = cx.generation();
 
-        *val += 1;
+        cx.spawn(async {
+            tokio::time::sleep(std::time::Duration::from_millis(100000)).await;
+        });
 
-        if *val == 2 || *val == 4 {
+        if val == 2 || val == 4 {
             return cx.render(rsx!(()));
         }
 
-        let name = cx.use_hook(|| String::from("asd"));
+        let name = cx.use_hook(|| String::from("numbers: "));
+
+        name.push_str("123 ");
 
         cx.render(rsx!(
             div { "Hello, world!" }
@@ -36,24 +32,26 @@ fn test_memory_leak() {
             Child {}
             Child {}
             Child {}
-            BorrowedChild { na: name }
-            BorrowedChild { na: name }
-            BorrowedChild { na: name }
-            BorrowedChild { na: name }
-            BorrowedChild { na: name }
+            BorrowedChild { name: name }
+            BorrowedChild { name: name }
+            BorrowedChild { name: name }
+            BorrowedChild { name: name }
+            BorrowedChild { name: name }
         ))
     }
 
     #[derive(Props)]
     struct BorrowedProps<'a> {
-        na: &'a str,
+        name: &'a str,
     }
 
     fn BorrowedChild<'a>(cx: Scope<'a, BorrowedProps<'a>>) -> Element {
-        render!(div {
-            "goodbye {cx.props.na}"
-            Child {}
-            Child {}
+        cx.render(rsx! {
+            div {
+                "goodbye {cx.props.name}"
+                Child {}
+                Child {}
+            }
         })
     }
 
@@ -63,24 +61,21 @@ fn test_memory_leak() {
 
     let mut dom = VirtualDom::new(app);
 
-    dom.rebuild();
-    dom.hard_diff(ScopeId(0));
-    dom.hard_diff(ScopeId(0));
-    dom.hard_diff(ScopeId(0));
-    dom.hard_diff(ScopeId(0));
-    dom.hard_diff(ScopeId(0));
-    dom.hard_diff(ScopeId(0));
+    _ = dom.rebuild();
+
+    for _ in 0..5 {
+        dom.mark_dirty(ScopeId(0));
+        _ = dom.render_immediate();
+    }
 }
 
 #[test]
 fn memo_works_properly() {
     fn app(cx: Scope) -> Element {
-        let val = cx.use_hook(|| 0);
+        let val = cx.generation();
 
-        *val += 1;
-
-        if *val == 2 || *val == 4 {
-            return None;
+        if val == 2 || val == 4 {
+            return cx.render(rsx!(()));
         }
 
         let name = cx.use_hook(|| String::from("asd"));
@@ -100,309 +95,257 @@ fn memo_works_properly() {
         render!(div { "goodbye world" })
     }
 
-    let mut dom = new_dom(app, ());
-
-    dom.rebuild();
-    dom.hard_diff(ScopeId(0));
-    dom.hard_diff(ScopeId(0));
-    dom.hard_diff(ScopeId(0));
-    dom.hard_diff(ScopeId(0));
-    dom.hard_diff(ScopeId(0));
-    dom.hard_diff(ScopeId(0));
-    dom.hard_diff(ScopeId(0));
-}
-
-#[test]
-fn free_works_on_root_props() {
-    fn app(cx: Scope<Custom>) -> Element {
-        cx.render(rsx! {
-            Child { a: "alpha"}
-            Child { a: "beta"}
-            Child { a: "gamma"}
-            Child { a: "delta"}
-        })
-    }
-
-    #[derive(Props, PartialEq)]
-    struct ChildProps {
-        a: &'static str,
-    }
-
-    fn Child(cx: Scope<ChildProps>) -> Element {
-        render!("child {cx.props.a}")
-    }
-
-    struct Custom {
-        val: String,
-    }
-
-    impl Drop for Custom {
-        fn drop(&mut self) {
-            dbg!("dropped! {}", &self.val);
-        }
-    }
+    let mut dom = VirtualDom::new(app);
 
-    let mut dom = new_dom(app, Custom { val: String::from("asd") });
     dom.rebuild();
-}
-
-#[test]
-fn free_works_on_borrowed() {
-    fn app(cx: Scope) -> Element {
-        cx.render(rsx! {
-            Child { a: "alpha", b: "asd".to_string() }
-        })
-    }
-    #[derive(Props)]
-    struct ChildProps<'a> {
-        a: &'a str,
-        b: String,
-    }
-
-    fn Child<'a>(cx: Scope<'a, ChildProps<'a>>) -> Element {
-        dbg!("rendering child");
-        render!("child {cx.props.a}, {cx.props.b}")
-    }
-
-    impl Drop for ChildProps<'_> {
-        fn drop(&mut self) {
-            dbg!("dropped child!");
-        }
-    }
-
-    let mut dom = new_dom(app, ());
-    let _ = dom.rebuild();
+    todo!()
+    // dom.hard_diff(ScopeId(0));
+    // dom.hard_diff(ScopeId(0));
+    // dom.hard_diff(ScopeId(0));
+    // dom.hard_diff(ScopeId(0));
+    // dom.hard_diff(ScopeId(0));
+    // dom.hard_diff(ScopeId(0));
+    // dom.hard_diff(ScopeId(0));
 }
 
 #[test]
 fn free_works_on_root_hooks() {
     /*
-    On Drop, scopearena drops all the hook contents.
+    On Drop, scopearena drops all the hook contents. and props
     */
-
-    struct Droppable<T>(T);
-    impl<T> Drop for Droppable<T> {
-        fn drop(&mut self) {
-            dbg!("dropping droppable");
-        }
+    #[derive(PartialEq, Clone, Props)]
+    struct AppProps {
+        inner: Rc<String>,
     }
 
-    fn app(cx: Scope) -> Element {
-        let name = cx.use_hook(|| Droppable(String::from("asd")));
-        render!(div { "{name.0}" })
+    fn app(cx: Scope<AppProps>) -> Element {
+        let name: &AppProps = cx.use_hook(|| cx.props.clone());
+        render!(child_component { inner: name.inner.clone() })
     }
 
-    let mut dom = new_dom(app, ());
-    let _ = dom.rebuild();
-}
-
-#[test]
-fn old_props_arent_stale() {
-    fn app(cx: Scope) -> Element {
-        dbg!("rendering parent");
-        let cnt = cx.use_hook(|| 0);
-        *cnt += 1;
-
-        if *cnt == 1 {
-            render!(div { Child { a: "abcdef".to_string() } })
-        } else {
-            render!(div { Child { a: "abcdef".to_string() } })
-        }
+    fn child_component(cx: Scope<AppProps>) -> Element {
+        render!(div { "{cx.props.inner}" })
     }
 
-    #[derive(Props, PartialEq)]
-    struct ChildProps {
-        a: String,
-    }
-    fn Child(cx: Scope<ChildProps>) -> Element {
-        dbg!("rendering child", &cx.props.a);
-        render!(div { "child {cx.props.a}" })
-    }
-
-    let mut dom = new_dom(app, ());
+    let ptr = Rc::new("asdasd".to_string());
+    let mut dom = VirtualDom::new_with_props(app, AppProps { inner: ptr.clone() });
     let _ = dom.rebuild();
 
-    dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
-    dom.work_with_deadline(|| false);
-
-    dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
-    dom.work_with_deadline(|| false);
+    // ptr gets cloned into props and then into the hook
+    assert_eq!(Rc::strong_count(&ptr), 4);
 
-    dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
-    dom.work_with_deadline(|| false);
+    drop(dom);
 
-    dbg!("forcing update to child");
-
-    dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
-    dom.work_with_deadline(|| false);
-
-    dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
-    dom.work_with_deadline(|| false);
-
-    dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
-    dom.work_with_deadline(|| false);
+    assert_eq!(Rc::strong_count(&ptr), 1);
 }
 
-#[test]
-fn basic() {
-    fn app(cx: Scope) -> Element {
-        render!(div {
-            Child { a: "abcdef".to_string() }
-        })
-    }
-
-    #[derive(Props, PartialEq)]
-    struct ChildProps {
-        a: String,
-    }
-
-    fn Child(cx: Scope<ChildProps>) -> Element {
-        dbg!("rendering child", &cx.props.a);
-        render!(div { "child {cx.props.a}" })
-    }
-
-    let mut dom = new_dom(app, ());
-    let _ = dom.rebuild();
-
-    dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
-    dom.work_with_deadline(|| false);
-
-    dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
-    dom.work_with_deadline(|| false);
-}
-
-#[test]
-fn leak_thru_children() {
-    fn app(cx: Scope) -> Element {
-        cx.render(rsx! {
-            Child {
-                name: "asd".to_string(),
-            }
-        });
-        cx.render(rsx! {
-            div {}
-        })
-    }
-
-    #[inline_props]
-    fn Child(cx: Scope, name: String) -> Element {
-        render!(div { "child {name}" })
-    }
-
-    let mut dom = new_dom(app, ());
-    let _ = dom.rebuild();
-
-    dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
-    dom.work_with_deadline(|| false);
-
-    dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
-    dom.work_with_deadline(|| false);
-}
-
-#[test]
-fn test_pass_thru() {
-    #[inline_props]
-    fn NavContainer<'a>(cx: Scope, children: Element<'a>) -> Element {
-        cx.render(rsx! {
-            header {
-                nav { children }
-            }
-        })
-    }
-
-    fn NavMenu(cx: Scope) -> Element {
-        render!(            NavBrand {}
-            div {
-                NavStart {}
-                NavEnd {}
-            }
-        )
-    }
-
-    fn NavBrand(cx: Scope) -> Element {
-        render!(div {})
-    }
-
-    fn NavStart(cx: Scope) -> Element {
-        render!(div {})
-    }
-
-    fn NavEnd(cx: Scope) -> Element {
-        render!(div {})
-    }
-
-    #[inline_props]
-    fn MainContainer<'a>(
-        cx: Scope,
-        nav: Element<'a>,
-        body: Element<'a>,
-        footer: Element<'a>,
-    ) -> Element {
-        cx.render(rsx! {
-            div {
-                class: "columns is-mobile",
-                div {
-                    class: "column is-full",
-                    nav,
-                    body,
-                    footer,
-                }
-            }
-        })
-    }
-
-    fn app(cx: Scope) -> Element {
-        let nav = cx.render(rsx! {
-            NavContainer {
-                NavMenu {}
-            }
-        });
-        let body = cx.render(rsx! {
-            div {}
-        });
-        let footer = cx.render(rsx! {
-            div {}
-        });
-
-        cx.render(rsx! {
-            MainContainer {
-                nav: nav,
-                body: body,
-                footer: footer,
-            }
-        })
-    }
-
-    let mut dom = new_dom(app, ());
-    let _ = dom.rebuild();
-
-    for _ in 0..40 {
-        dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
-        dom.work_with_deadline(|| false);
-        dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
-        dom.work_with_deadline(|| false);
-        dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
-        dom.work_with_deadline(|| false);
-
-        dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
-        dom.work_with_deadline(|| false);
-        dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
-        dom.work_with_deadline(|| false);
-        dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
-        dom.work_with_deadline(|| false);
-
-        dom.handle_message(SchedulerMsg::Immediate(ScopeId(2)));
-        dom.work_with_deadline(|| false);
-        dom.handle_message(SchedulerMsg::Immediate(ScopeId(2)));
-        dom.work_with_deadline(|| false);
-        dom.handle_message(SchedulerMsg::Immediate(ScopeId(2)));
-        dom.work_with_deadline(|| false);
-
-        dom.handle_message(SchedulerMsg::Immediate(ScopeId(3)));
-        dom.work_with_deadline(|| false);
-        dom.handle_message(SchedulerMsg::Immediate(ScopeId(3)));
-        dom.work_with_deadline(|| false);
-        dom.handle_message(SchedulerMsg::Immediate(ScopeId(3)));
-        dom.work_with_deadline(|| false);
-    }
-}
+// #[test]
+// fn old_props_arent_stale() {
+//     fn app(cx: Scope) -> Element {
+//         dbg!("rendering parent");
+//         let cnt = cx.use_hook(|| 0);
+//         *cnt += 1;
+
+//         if *cnt == 1 {
+//             render!(div { Child { a: "abcdef".to_string() } })
+//         } else {
+//             render!(div { Child { a: "abcdef".to_string() } })
+//         }
+//     }
+
+//     #[derive(Props, PartialEq)]
+//     struct ChildProps {
+//         a: String,
+//     }
+//     fn Child(cx: Scope<ChildProps>) -> Element {
+//         dbg!("rendering child", &cx.props.a);
+//         render!(div { "child {cx.props.a}" })
+//     }
+
+//     let mut dom = new_dom(app, ());
+//     let _ = dom.rebuild();
+
+//     dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
+//     dom.work_with_deadline(|| false);
+
+//     dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
+//     dom.work_with_deadline(|| false);
+
+//     dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
+//     dom.work_with_deadline(|| false);
+
+//     dbg!("forcing update to child");
+
+//     dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
+//     dom.work_with_deadline(|| false);
+
+//     dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
+//     dom.work_with_deadline(|| false);
+
+//     dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
+//     dom.work_with_deadline(|| false);
+// }
+
+// #[test]
+// fn basic() {
+//     fn app(cx: Scope) -> Element {
+//         render!(div {
+//             Child { a: "abcdef".to_string() }
+//         })
+//     }
+
+//     #[derive(Props, PartialEq)]
+//     struct ChildProps {
+//         a: String,
+//     }
+
+//     fn Child(cx: Scope<ChildProps>) -> Element {
+//         dbg!("rendering child", &cx.props.a);
+//         render!(div { "child {cx.props.a}" })
+//     }
+
+//     let mut dom = new_dom(app, ());
+//     let _ = dom.rebuild();
+
+//     dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
+//     dom.work_with_deadline(|| false);
+
+//     dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
+//     dom.work_with_deadline(|| false);
+// }
+
+// #[test]
+// fn leak_thru_children() {
+//     fn app(cx: Scope) -> Element {
+//         cx.render(rsx! {
+//             Child {
+//                 name: "asd".to_string(),
+//             }
+//         });
+//         cx.render(rsx! {
+//             div {}
+//         })
+//     }
+
+//     #[inline_props]
+//     fn Child(cx: Scope, name: String) -> Element {
+//         render!(div { "child {name}" })
+//     }
+
+//     let mut dom = new_dom(app, ());
+//     let _ = dom.rebuild();
+
+//     dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
+//     dom.work_with_deadline(|| false);
+
+//     dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
+//     dom.work_with_deadline(|| false);
+// }
+
+// #[test]
+// fn test_pass_thru() {
+//     #[inline_props]
+//     fn NavContainer<'a>(cx: Scope, children: Element<'a>) -> Element {
+//         cx.render(rsx! {
+//             header {
+//                 nav { children }
+//             }
+//         })
+//     }
+
+//     fn NavMenu(cx: Scope) -> Element {
+//         render!(            NavBrand {}
+//             div {
+//                 NavStart {}
+//                 NavEnd {}
+//             }
+//         )
+//     }
+
+//     fn NavBrand(cx: Scope) -> Element {
+//         render!(div {})
+//     }
+
+//     fn NavStart(cx: Scope) -> Element {
+//         render!(div {})
+//     }
+
+//     fn NavEnd(cx: Scope) -> Element {
+//         render!(div {})
+//     }
+
+//     #[inline_props]
+//     fn MainContainer<'a>(
+//         cx: Scope,
+//         nav: Element<'a>,
+//         body: Element<'a>,
+//         footer: Element<'a>,
+//     ) -> Element {
+//         cx.render(rsx! {
+//             div {
+//                 class: "columns is-mobile",
+//                 div {
+//                     class: "column is-full",
+//                     nav,
+//                     body,
+//                     footer,
+//                 }
+//             }
+//         })
+//     }
+
+//     fn app(cx: Scope) -> Element {
+//         let nav = cx.render(rsx! {
+//             NavContainer {
+//                 NavMenu {}
+//             }
+//         });
+//         let body = cx.render(rsx! {
+//             div {}
+//         });
+//         let footer = cx.render(rsx! {
+//             div {}
+//         });
+
+//         cx.render(rsx! {
+//             MainContainer {
+//                 nav: nav,
+//                 body: body,
+//                 footer: footer,
+//             }
+//         })
+//     }
+
+//     let mut dom = new_dom(app, ());
+//     let _ = dom.rebuild();
+
+//     for _ in 0..40 {
+//         dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
+//         dom.work_with_deadline(|| false);
+//         dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
+//         dom.work_with_deadline(|| false);
+//         dom.handle_message(SchedulerMsg::Immediate(ScopeId(0)));
+//         dom.work_with_deadline(|| false);
+
+//         dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
+//         dom.work_with_deadline(|| false);
+//         dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
+//         dom.work_with_deadline(|| false);
+//         dom.handle_message(SchedulerMsg::Immediate(ScopeId(1)));
+//         dom.work_with_deadline(|| false);
+
+//         dom.handle_message(SchedulerMsg::Immediate(ScopeId(2)));
+//         dom.work_with_deadline(|| false);
+//         dom.handle_message(SchedulerMsg::Immediate(ScopeId(2)));
+//         dom.work_with_deadline(|| false);
+//         dom.handle_message(SchedulerMsg::Immediate(ScopeId(2)));
+//         dom.work_with_deadline(|| false);
+
+//         dom.handle_message(SchedulerMsg::Immediate(ScopeId(3)));
+//         dom.work_with_deadline(|| false);
+//         dom.handle_message(SchedulerMsg::Immediate(ScopeId(3)));
+//         dom.work_with_deadline(|| false);
+//         dom.handle_message(SchedulerMsg::Immediate(ScopeId(3)));
+//         dom.work_with_deadline(|| false);
+//     }
+// }

+ 0 - 4
packages/core/tests/safety.rs

@@ -13,10 +13,6 @@ fn root_node_isnt_null() {
     // We haven't built the tree, so trying to get out the root node should fail
     assert!(scope.try_root_node().is_none());
 
-    // There should be no way to gain an invalid pointer
-    assert!(scope.current_frame().node.get().is_null());
-    assert!(scope.previous_frame().node.get().is_null());
-
     // The height should be 0
     assert_eq!(scope.height(), 0);
 

+ 1 - 1
packages/core/tests/suspense.rs

@@ -11,7 +11,7 @@ async fn it_works() {
 
     let mutations = dom.rebuild().santize();
 
-    // We should at least get the top-level template in
+    // We should at least get the top-level template in before pausing for the children
     assert_eq!(
         mutations.template_mutations,
         [

+ 1 - 1
packages/core/tests/task.rs

@@ -13,7 +13,7 @@ async fn it_works() {
 
     tokio::select! {
         _ = dom.wait_for_work() => {}
-        _ = tokio::time::sleep(Duration::from_millis(10)) => {}
+        _ = tokio::time::sleep(Duration::from_millis(500)) => {}
     };
 
     // By the time the tasks are finished, we should've accumulated ticks from two tasks

+ 0 - 1
packages/rsx/src/lib.rs

@@ -144,7 +144,6 @@ impl<'a> ToTokens for TemplateRenderer<'a> {
                 attr_paths: &[ #(#attr_paths),* ],
             };
             ::dioxus::core::VNode {
-                node_id: Default::default(),
                 parent: None,
                 key: #key_tokens,
                 template: TEMPLATE,