Prechádzať zdrojové kódy

feat: make hooks free-functions

Jonathan Kelley 3 rokov pred
rodič
commit
e5c88fe

+ 1 - 0
README.md

@@ -84,6 +84,7 @@ If you know React, then you already know Dioxus.
 - Starting a new app takes zero templates or special tools - get a new app running in just seconds.
 - Desktop apps running natively (no Electron!) in less than 10 lines of code.
 - The most ergonomic and powerful state management of any Rust UI toolkit.
+- Multithreaded asynchronous coroutine scheduler for powerful async code.
 - And more! Read the full release post here.
 
 ## Get Started with...

+ 1 - 1
examples/async.rs

@@ -12,7 +12,7 @@ pub static App: FC<()> = |cx| {
     let mut direction = use_state(cx, || 1);
 
     let (async_count, dir) = (count.for_async(), *direction);
-    let (task, _result) = cx.use_task(move || async move {
+    let (task, _result) = use_task(cx, move || async move {
         loop {
             gloo_timers::future::TimeoutFuture::new(250).await;
             *async_count.get_mut() += dir;

+ 2 - 2
examples/coroutine.rs

@@ -32,13 +32,13 @@ static App: FC<()> = |cx| {
     let p2 = use_state(cx, || 0);
 
     let (mut p1_async, mut p2_async) = (p1.for_async(), p2.for_async());
-    let (p1_handle, _) = cx.use_task(|| async move {
+    let (p1_handle, _) = use_task(cx, || async move {
         loop {
             *p1_async.get_mut() += 1;
             async_std::task::sleep(std::time::Duration::from_millis(75)).await;
         }
     });
-    let (p2_handle, _) = cx.use_task(|| async move {
+    let (p2_handle, _) = use_task(cx, || async move {
         loop {
             *p2_async.get_mut() += 1;
             async_std::task::sleep(std::time::Duration::from_millis(100)).await;

+ 2 - 1
examples/reference/suspense.rs

@@ -15,7 +15,8 @@ struct DogApi {
 const ENDPOINT: &str = "https://dog.ceo/api/breeds/image/random";
 
 pub static Example: FC<()> = |cx| {
-    let doggo = cx.use_suspense(
+    let doggo = use_suspense(
+        cx,
         || surf::get(ENDPOINT).recv_json::<DogApi>(),
         |cx, res| match res {
             Ok(res) => rsx!(in cx, img { src: "{res.message}" }),

+ 1 - 1
examples/reference/task.rs

@@ -30,7 +30,7 @@ pub static Example: FC<()> = |cx| {
 
     // Tasks are 'static, so we need to copy relevant items in
     let (async_count, dir) = (count.for_async(), *direction);
-    let (task, result) = cx.use_task(move || async move {
+    let (task, result) = use_task(cx, move || async move {
         // Count infinitely!
         loop {
             gloo_timers::future::TimeoutFuture::new(250).await;

+ 2 - 2
packages/core/examples/async.rs

@@ -21,7 +21,7 @@ const App: FC<()> = |cx| {
 };
 
 const Task: FC<()> = |cx| {
-    let (task, res) = cx.use_task(|| async { true });
+    let (task, res) = use_task(cx, || async { true });
     // task.pause();
     // task.restart();
     // task.stop();
@@ -29,7 +29,7 @@ const Task: FC<()> = |cx| {
 
     //
 
-    let _s = cx.use_task(|| async { "hello world".to_string() });
+    let _s = use_task(cx, || async { "hello world".to_string() });
 
     todo!()
 };

+ 2 - 1
packages/core/examples/fragment_from_iter.rs

@@ -4,7 +4,8 @@ use dioxus_core::prelude::*;
 
 fn App(cx: Context<()>) -> DomTree {
     //
-    let vak = cx.use_suspense(
+    let vak = use_suspense(
+        cx,
         || async {},
         |c, res| {
             //

+ 75 - 309
packages/core/src/context.rs

@@ -111,7 +111,7 @@ impl<'src, P> Context<'src, P> {
         Rc::new(move || cb(id))
     }
 
-    fn prepare_update(&self) -> Rc<dyn Fn(ScopeId)> {
+    pub fn prepare_update(&self) -> Rc<dyn Fn(ScopeId)> {
         self.scope.vdom.schedule_update()
     }
 
@@ -155,8 +155,82 @@ impl<'src, P> Context<'src, P> {
         }))
     }
 
+    /// `submit_task` will submit the future to be polled.
+    ///
+    /// This is useful when you have some async task that needs to be progressed.
+    ///
+    /// This method takes ownership over the task you've provided, and must return (). This means any work that needs to
+    /// happen must occur within the future or scheduled for after the future completes (through schedule_update )
+    ///
+    /// ## Explanation
+    /// Dioxus will step its internal event loop if the future returns if the future completes while waiting.
+    ///
+    /// Tasks can't return anything, but they can be controlled with the returned handle
+    ///
+    /// Tasks will only run until the component renders again. Because `submit_task` is valid for the &'src lifetime, it
+    /// is considered "stable"
+    ///
+    ///
+    ///
+    pub fn submit_task(&self, task: FiberTask) -> TaskHandle {
+        self.scope.vdom.submit_task(task)
+    }
+
+    /// Add a state globally accessible to child components via tree walking
+    pub fn add_shared_state<T: 'static>(self, val: T) -> Option<Rc<dyn Any>> {
+        self.scope
+            .shared_contexts
+            .borrow_mut()
+            .insert(TypeId::of::<T>(), Rc::new(val))
+    }
+
+    /// Walk the tree to find a shared state with the TypeId of the generic type
+    ///
+    pub fn consume_shared_state<T: 'static>(self) -> Option<Rc<T>> {
+        let mut scope = Some(self.scope);
+        let mut par = None;
+
+        let ty = TypeId::of::<T>();
+        while let Some(inner) = scope {
+            log::debug!(
+                "Searching {:#?} for valid shared_context",
+                inner.our_arena_idx
+            );
+            let shared_ctx = {
+                let shared_contexts = inner.shared_contexts.borrow();
+
+                log::debug!(
+                    "This component has {} shared contexts",
+                    shared_contexts.len()
+                );
+                shared_contexts.get(&ty).map(|f| f.clone())
+            };
+
+            if let Some(shared_cx) = shared_ctx {
+                log::debug!("found matching cx");
+                let rc = shared_cx
+                    .clone()
+                    .downcast::<T>()
+                    .expect("Should not fail, already validated the type from the hashmap");
+
+                par = Some(rc);
+                break;
+            } else {
+                match inner.parent_idx {
+                    Some(parent_id) => {
+                        scope = unsafe { inner.vdom.get_scope(parent_id) };
+                    }
+                    None => break,
+                }
+            }
+        }
+        par
+    }
+
     /// Store a value between renders
     ///
+    /// This is *the* foundational hook for all other hooks.
+    ///
     /// - Initializer: closure used to create the initial hook state
     /// - Runner: closure used to output a value every time the hook is used
     /// - Cleanup: closure used to teardown the hook once the dom is cleaned up
@@ -200,312 +274,4 @@ Any function prefixed with "use" should not be called conditionally.
 
         runner(self.scope.hooks.next::<State>().expect(ERR_MSG))
     }
-
-    /// This hook enables the ability to expose state to children further down the VirtualDOM Tree.
-    ///
-    /// This is a hook, so it may not be called conditionally!
-    ///
-    /// The init method is ran *only* on first use, otherwise it is ignored. However, it uses hooks (ie `use`)
-    /// so don't put it in a conditional.
-    ///
-    /// When the component is dropped, so is the context. Be aware of this behavior when consuming
-    /// the context via Rc/Weak.
-    ///
-    ///
-    ///
-    pub fn use_provide_context<T, F>(self, init: F) -> &'src Rc<T>
-    where
-        T: 'static,
-        F: FnOnce() -> T,
-    {
-        let ty = TypeId::of::<T>();
-        let contains_key = self.scope.shared_contexts.borrow().contains_key(&ty);
-
-        let is_initialized = self.use_hook(
-            |_| false,
-            |s| {
-                let i = s.clone();
-                *s = true;
-                i
-            },
-            |_| {},
-        );
-
-        match (is_initialized, contains_key) {
-            // Do nothing, already initialized and already exists
-            (true, true) => {}
-
-            // Needs to be initialized
-            (false, false) => {
-                log::debug!("Initializing context...");
-                let initialized = Rc::new(init());
-                let p = self
-                    .scope
-                    .shared_contexts
-                    .borrow_mut()
-                    .insert(ty, initialized);
-                log::info!(
-                    "There are now {} shared contexts for scope {:?}",
-                    self.scope.shared_contexts.borrow().len(),
-                    self.scope.our_arena_idx,
-                );
-            }
-
-            _ => debug_assert!(false, "Cannot initialize two contexts of the same type"),
-        };
-
-        self.use_context::<T>()
-    }
-
-    /// There are hooks going on here!
-    pub fn use_context<T: 'static>(self) -> &'src Rc<T> {
-        self.try_use_context().unwrap()
-    }
-
-    /// Uses a context, storing the cached value around
-    ///
-    /// If a context is not found on the first search, then this call will be  "dud", always returning "None" even if a
-    /// context was added later. This allows using another hook as a fallback
-    ///
-    pub fn try_use_context<T: 'static>(self) -> Option<&'src Rc<T>> {
-        struct UseContextHook<C> {
-            par: Option<Rc<C>>,
-        }
-
-        self.use_hook(
-            move |_| {
-                let mut scope = Some(self.scope);
-                let mut par = None;
-
-                let ty = TypeId::of::<T>();
-                while let Some(inner) = scope {
-                    log::debug!(
-                        "Searching {:#?} for valid shared_context",
-                        inner.our_arena_idx
-                    );
-                    let shared_ctx = {
-                        let shared_contexts = inner.shared_contexts.borrow();
-
-                        log::debug!(
-                            "This component has {} shared contexts",
-                            shared_contexts.len()
-                        );
-                        shared_contexts.get(&ty).map(|f| f.clone())
-                    };
-
-                    if let Some(shared_cx) = shared_ctx {
-                        log::debug!("found matching cx");
-                        let rc = shared_cx
-                            .clone()
-                            .downcast::<T>()
-                            .expect("Should not fail, already validated the type from the hashmap");
-
-                        par = Some(rc);
-                        break;
-                    } else {
-                        match inner.parent_idx {
-                            Some(parent_id) => {
-                                scope = unsafe { inner.vdom.get_scope(parent_id) };
-                            }
-                            None => break,
-                        }
-                    }
-                }
-                //
-                UseContextHook { par }
-            },
-            move |hook| hook.par.as_ref(),
-            |_| {},
-        )
-    }
-
-    /// `submit_task` will submit the future to be polled.
-    ///
-    /// This is useful when you have some async task that needs to be progressed.
-    ///
-    /// This method takes ownership over the task you've provided, and must return (). This means any work that needs to
-    /// happen must occur within the future or scheduled for after the future completes (through schedule_update )
-    ///
-    /// ## Explanation
-    /// Dioxus will step its internal event loop if the future returns if the future completes while waiting.
-    ///
-    /// Tasks can't return anything, but they can be controlled with the returned handle
-    ///
-    /// Tasks will only run until the component renders again. Because `submit_task` is valid for the &'src lifetime, it
-    /// is considered "stable"
-    ///
-    ///
-    ///
-    pub fn submit_task(&self, task: FiberTask) -> TaskHandle {
-        self.scope.vdom.submit_task(task)
-    }
-
-    /// Awaits the given task, forcing the component to re-render when the value is ready.
-    ///
-    ///
-    ///
-    ///
-    pub fn use_task<Out, Fut, Init>(
-        self,
-        task_initializer: Init,
-    ) -> (&'src TaskHandle, &'src Option<Out>)
-    where
-        Out: 'static,
-        Fut: Future<Output = Out> + 'static,
-        Init: FnOnce() -> Fut + 'src,
-    {
-        struct TaskHook<T> {
-            handle: TaskHandle,
-            task_dump: Rc<RefCell<Option<T>>>,
-            value: Option<T>,
-        }
-
-        // whenever the task is complete, save it into th
-        self.use_hook(
-            move |hook_idx| {
-                let task_fut = task_initializer();
-
-                let task_dump = Rc::new(RefCell::new(None));
-
-                let slot = task_dump.clone();
-
-                let updater = self.prepare_update();
-                let update_id = self.get_scope_id();
-
-                let originator = self.scope.our_arena_idx.clone();
-
-                let handle = self.submit_task(Box::pin(task_fut.then(move |output| async move {
-                    *slot.as_ref().borrow_mut() = Some(output);
-                    updater(update_id);
-                    EventTrigger {
-                        event: VirtualEvent::AsyncEvent { hook_idx },
-                        originator,
-                        priority: EventPriority::Low,
-                        real_node_id: None,
-                    }
-                })));
-
-                TaskHook {
-                    task_dump,
-                    value: None,
-                    handle,
-                }
-            },
-            |hook| {
-                if let Some(val) = hook.task_dump.as_ref().borrow_mut().take() {
-                    hook.value = Some(val);
-                }
-                (&hook.handle, &hook.value)
-            },
-            |_| {},
-        )
-    }
-}
-
-pub(crate) struct SuspenseHook {
-    pub value: Rc<RefCell<Option<Box<dyn Any>>>>,
-    pub callback: SuspendedCallback,
-    pub dom_node_id: Rc<Cell<Option<ElementId>>>,
-}
-type SuspendedCallback = Box<dyn for<'a> Fn(SuspendedContext<'a>) -> DomTree<'a>>;
-
-impl<'src, P> Context<'src, P> {
-    /// Asynchronously render new nodes once the given future has completed.
-    ///
-    /// # Easda
-    ///
-    ///
-    ///
-    ///
-    /// # Example
-    ///
-    ///
-    pub fn use_suspense<Out, Fut, Cb>(
-        self,
-        task_initializer: impl FnOnce() -> Fut,
-        user_callback: Cb,
-    ) -> DomTree<'src>
-    where
-        Fut: Future<Output = Out> + 'static,
-        Out: 'static,
-        Cb: for<'a> Fn(SuspendedContext<'a>, &Out) -> DomTree<'a> + 'static,
-    {
-        self.use_hook(
-            move |hook_idx| {
-                let value = Rc::new(RefCell::new(None));
-
-                let dom_node_id = Rc::new(empty_cell());
-                let domnode = dom_node_id.clone();
-
-                let slot = value.clone();
-
-                let callback: SuspendedCallback = Box::new(move |ctx: SuspendedContext| {
-                    let v: std::cell::Ref<Option<Box<dyn Any>>> = slot.as_ref().borrow();
-                    match v.as_ref() {
-                        Some(a) => {
-                            let v: &dyn Any = a.as_ref();
-                            let real_val = v.downcast_ref::<Out>().unwrap();
-                            user_callback(ctx, real_val)
-                        }
-                        None => {
-                            //
-                            Some(VNode {
-                                dom_id: empty_cell(),
-                                key: None,
-                                kind: VNodeKind::Suspended {
-                                    node: domnode.clone(),
-                                },
-                            })
-                        }
-                    }
-                });
-
-                let originator = self.scope.our_arena_idx.clone();
-                let task_fut = task_initializer();
-                let domnode = dom_node_id.clone();
-
-                let slot = value.clone();
-                self.submit_task(Box::pin(task_fut.then(move |output| async move {
-                    // When the new value arrives, set the hooks internal slot
-                    // Dioxus will call the user's callback to generate new nodes outside of the diffing system
-                    *slot.borrow_mut() = Some(Box::new(output) as Box<dyn Any>);
-                    EventTrigger {
-                        event: VirtualEvent::SuspenseEvent { hook_idx, domnode },
-                        originator,
-                        priority: EventPriority::Low,
-                        real_node_id: None,
-                    }
-                })));
-
-                SuspenseHook {
-                    value,
-                    callback,
-                    dom_node_id,
-                }
-            },
-            move |hook| {
-                let cx = Context {
-                    scope: &self.scope,
-                    props: &(),
-                };
-                let csx = SuspendedContext { inner: cx };
-                (&hook.callback)(csx)
-            },
-            |_| {},
-        )
-    }
-}
-
-pub struct SuspendedContext<'a> {
-    pub(crate) inner: Context<'a, ()>,
-}
-impl<'src> SuspendedContext<'src> {
-    pub fn render<F: FnOnce(NodeFactory<'src>) -> VNode<'src>>(
-        self,
-        lazy_nodes: LazyNodes<'src, F>,
-    ) -> DomTree<'src> {
-        let scope_ref = self.inner.scope;
-
-        Some(lazy_nodes.into_vnode(NodeFactory { scope: scope_ref }))
-    }
 }

+ 256 - 0
packages/core/src/hooks.rs

@@ -0,0 +1,256 @@
+//! Built-in hooks
+//!
+//! This module contains all the low-level built-in hooks that require 1st party support to work.
+//!
+//! Hooks:
+//! - use_hook
+//! - use_state_provider
+//! - use_state_consumer
+//! - use_task
+//! - use_suspense
+
+use crate::innerlude::*;
+use futures_util::FutureExt;
+use std::{
+    any::{Any, TypeId},
+    cell::{Cell, RefCell},
+    future::Future,
+    rc::Rc,
+};
+
+/// This hook enables the ability to expose state to children further down the VirtualDOM Tree.
+///
+/// This is a hook, so it may not be called conditionally!
+///
+/// The init method is ran *only* on first use, otherwise it is ignored. However, it uses hooks (ie `use`)
+/// so don't put it in a conditional.
+///
+/// When the component is dropped, so is the context. Be aware of this behavior when consuming
+/// the context via Rc/Weak.
+///
+///
+///
+pub fn use_provide_state<'src, Pr, T, F>(cx: Context<'src, Pr>, init: F) -> &'src Rc<T>
+where
+    T: 'static,
+    F: FnOnce() -> T,
+{
+    let ty = TypeId::of::<T>();
+    let contains_key = cx.scope.shared_contexts.borrow().contains_key(&ty);
+
+    let is_initialized = cx.use_hook(
+        |_| false,
+        |s| {
+            let i = s.clone();
+            *s = true;
+            i
+        },
+        |_| {},
+    );
+
+    match (is_initialized, contains_key) {
+        // Do nothing, already initialized and already exists
+        (true, true) => {}
+
+        // Needs to be initialized
+        (false, false) => {
+            log::debug!("Initializing context...");
+            cx.add_shared_state(init());
+            log::info!(
+                "There are now {} shared contexts for scope {:?}",
+                cx.scope.shared_contexts.borrow().len(),
+                cx.scope.our_arena_idx,
+            );
+        }
+
+        _ => debug_assert!(false, "Cannot initialize two contexts of the same type"),
+    };
+
+    use_consume_state::<T, _>(cx)
+}
+
+/// There are hooks going on here!
+pub fn use_consume_state<'src, T: 'static, P>(cx: Context<'src, P>) -> &'src Rc<T> {
+    use_try_consume_state::<T, _>(cx).unwrap()
+}
+
+/// Uses a context, storing the cached value around
+///
+/// If a context is not found on the first search, then this call will be  "dud", always returning "None" even if a
+/// context was added later. This allows using another hook as a fallback
+///
+pub fn use_try_consume_state<'src, T: 'static, P>(cx: Context<'src, P>) -> Option<&'src Rc<T>> {
+    struct UseContextHook<C>(Option<Rc<C>>);
+
+    cx.use_hook(
+        move |_| UseContextHook(cx.consume_shared_state::<T>()),
+        move |hook| hook.0.as_ref(),
+        |_| {},
+    )
+}
+
+/// Awaits the given task, forcing the component to re-render when the value is ready.
+///
+///
+///
+///
+pub fn use_task<'src, Out, Fut, Init, P>(
+    cx: Context<'src, P>,
+    task_initializer: Init,
+) -> (&'src TaskHandle, &'src Option<Out>)
+where
+    Out: 'static,
+    Fut: Future<Output = Out> + 'static,
+    Init: FnOnce() -> Fut + 'src,
+{
+    struct TaskHook<T> {
+        handle: TaskHandle,
+        task_dump: Rc<RefCell<Option<T>>>,
+        value: Option<T>,
+    }
+
+    // whenever the task is complete, save it into th
+    cx.use_hook(
+        move |hook_idx| {
+            let task_fut = task_initializer();
+
+            let task_dump = Rc::new(RefCell::new(None));
+
+            let slot = task_dump.clone();
+
+            let updater = cx.prepare_update();
+            let update_id = cx.get_scope_id();
+
+            let originator = cx.scope.our_arena_idx.clone();
+
+            let handle = cx.submit_task(Box::pin(task_fut.then(move |output| async move {
+                *slot.as_ref().borrow_mut() = Some(output);
+                updater(update_id);
+                EventTrigger {
+                    event: VirtualEvent::AsyncEvent { hook_idx },
+                    originator,
+                    priority: EventPriority::Low,
+                    real_node_id: None,
+                }
+            })));
+
+            TaskHook {
+                task_dump,
+                value: None,
+                handle,
+            }
+        },
+        |hook| {
+            if let Some(val) = hook.task_dump.as_ref().borrow_mut().take() {
+                hook.value = Some(val);
+            }
+            (&hook.handle, &hook.value)
+        },
+        |_| {},
+    )
+}
+
+/// Asynchronously render new nodes once the given future has completed.
+///
+/// # Easda
+///
+///
+///
+///
+/// # Example
+///
+///
+pub fn use_suspense<'src, Out, Fut, Cb, P>(
+    cx: Context<'src, P>,
+    task_initializer: impl FnOnce() -> Fut,
+    user_callback: Cb,
+) -> DomTree<'src>
+where
+    Fut: Future<Output = Out> + 'static,
+    Out: 'static,
+    Cb: for<'a> Fn(SuspendedContext<'a>, &Out) -> DomTree<'a> + 'static,
+{
+    cx.use_hook(
+        move |hook_idx| {
+            let value = Rc::new(RefCell::new(None));
+
+            let dom_node_id = Rc::new(empty_cell());
+            let domnode = dom_node_id.clone();
+
+            let slot = value.clone();
+
+            let callback: SuspendedCallback = Box::new(move |ctx: SuspendedContext| {
+                let v: std::cell::Ref<Option<Box<dyn Any>>> = slot.as_ref().borrow();
+                match v.as_ref() {
+                    Some(a) => {
+                        let v: &dyn Any = a.as_ref();
+                        let real_val = v.downcast_ref::<Out>().unwrap();
+                        user_callback(ctx, real_val)
+                    }
+                    None => {
+                        //
+                        Some(VNode {
+                            dom_id: empty_cell(),
+                            key: None,
+                            kind: VNodeKind::Suspended {
+                                node: domnode.clone(),
+                            },
+                        })
+                    }
+                }
+            });
+
+            let originator = cx.scope.our_arena_idx.clone();
+            let task_fut = task_initializer();
+            let domnode = dom_node_id.clone();
+
+            let slot = value.clone();
+            cx.submit_task(Box::pin(task_fut.then(move |output| async move {
+                // When the new value arrives, set the hooks internal slot
+                // Dioxus will call the user's callback to generate new nodes outside of the diffing system
+                *slot.borrow_mut() = Some(Box::new(output) as Box<dyn Any>);
+                EventTrigger {
+                    event: VirtualEvent::SuspenseEvent { hook_idx, domnode },
+                    originator,
+                    priority: EventPriority::Low,
+                    real_node_id: None,
+                }
+            })));
+
+            SuspenseHook {
+                value,
+                callback,
+                dom_node_id,
+            }
+        },
+        move |hook| {
+            let cx = Context {
+                scope: &cx.scope,
+                props: &(),
+            };
+            let csx = SuspendedContext { inner: cx };
+            (&hook.callback)(csx)
+        },
+        |_| {},
+    )
+}
+
+pub(crate) struct SuspenseHook {
+    pub value: Rc<RefCell<Option<Box<dyn Any>>>>,
+    pub callback: SuspendedCallback,
+    pub dom_node_id: Rc<Cell<Option<ElementId>>>,
+}
+type SuspendedCallback = Box<dyn for<'a> Fn(SuspendedContext<'a>) -> DomTree<'a>>;
+pub struct SuspendedContext<'a> {
+    pub(crate) inner: Context<'a, ()>,
+}
+impl<'src> SuspendedContext<'src> {
+    pub fn render<F: FnOnce(NodeFactory<'src>) -> VNode<'src>>(
+        self,
+        lazy_nodes: LazyNodes<'src, F>,
+    ) -> DomTree<'src> {
+        let scope_ref = self.inner.scope;
+
+        Some(lazy_nodes.into_vnode(NodeFactory { scope: scope_ref }))
+    }
+}

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

@@ -11,13 +11,14 @@
 
 pub use crate::innerlude::{
     format_args_f, html, rsx, Context, DioxusElement, DomEdit, DomTree, ElementId, EventPriority,
-    EventTrigger, LazyNodes, NodeFactory, Properties, RealDom, ScopeId, VNode, VNodeKind,
-    VirtualDom, VirtualEvent, FC,
+    EventTrigger, LazyNodes, NodeFactory, Properties, RealDom, ScopeId, SuspendedContext, VNode,
+    VNodeKind, VirtualDom, VirtualEvent, FC,
 };
 
 pub mod prelude {
     pub use crate::component::{fc_to_builder, Fragment, Properties};
     pub use crate::context::Context;
+    pub use crate::hooks::*;
     pub use crate::innerlude::{DioxusElement, DomTree, LazyNodes, NodeFactory, FC};
     pub use crate::nodes::VNode;
     pub use crate::VirtualDom;
@@ -36,6 +37,7 @@ pub(crate) mod innerlude {
     pub use crate::events::*;
     pub use crate::heuristics::*;
     pub use crate::hooklist::*;
+    pub use crate::hooks::*;
     pub use crate::nodes::*;
     pub use crate::scope::*;
     pub use crate::util::*;
@@ -62,6 +64,7 @@ pub mod error;
 pub mod events;
 pub mod heuristics;
 pub mod hooklist;
+pub mod hooks;
 pub mod nodes;
 pub mod scope;
 pub mod signals;

+ 23 - 3
packages/core/src/nodes.rs

@@ -12,6 +12,7 @@ use std::{
     cell::{Cell, RefCell},
     fmt::{Arguments, Debug, Formatter},
     marker::PhantomData,
+    mem::ManuallyDrop,
     rc::Rc,
 };
 
@@ -127,6 +128,8 @@ pub struct VComponent<'src> {
 
     pub(crate) comparator: Option<&'src dyn Fn(&VComponent) -> bool>,
 
+    pub(crate) drop_props: Option<&'src dyn FnOnce()>,
+
     pub is_static: bool,
 
     // a pointer into the bump arena (given by the 'src lifetime)
@@ -335,15 +338,22 @@ impl<'a> NodeFactory<'a> {
         // We don't want the fat part of the fat pointer
         // This function does static dispatch so we don't need any VTable stuff
         let props = self.bump().alloc(props);
-        let raw_props = props as *const P as *const ();
 
+        let raw_props = props as *mut P as *mut ();
         let user_fc = component as *const ();
 
         let comparator: Option<&dyn Fn(&VComponent) -> bool> = Some(self.bump().alloc_with(|| {
             move |other: &VComponent| {
                 if user_fc == other.user_fc {
-                    let real_other = unsafe { &*(other.raw_props as *const _ as *const P) };
-                    let props_memoized = unsafe { props.memoize(&real_other) };
+                    // Safety
+                    // - We guarantee that FC<P> is the same by function pointer
+                    // - Because FC<P> is the same, then P must be the same (even with generics)
+                    // - Non-static P are autoderived to memoize as false
+                    // - This comparator is only called on a corresponding set of bumpframes
+                    let props_memoized = unsafe {
+                        let real_other: &P = &*(other.raw_props as *const _ as *const P);
+                        props.memoize(&real_other)
+                    };
 
                     // It's only okay to memoize if there are no children and the props can be memoized
                     // Implementing memoize is unsafe and done automatically with the props trait
@@ -357,6 +367,15 @@ impl<'a> NodeFactory<'a> {
             }
         }));
 
+        // create a closure to drop the props
+        let drop_props: Option<&dyn FnOnce()> = Some(self.bump().alloc_with(|| {
+            move || unsafe {
+                let real_other = raw_props as *mut _ as *mut P;
+                let b = BumpBox::from_raw(real_other);
+                std::mem::drop(b);
+            }
+        }));
+
         let is_static = children.len() == 0 && P::IS_STATIC && key.is_none();
 
         VNode {
@@ -369,6 +388,7 @@ impl<'a> NodeFactory<'a> {
                 children,
                 caller: NodeFactory::create_component_caller(component, raw_props),
                 is_static,
+                drop_props,
                 ass_scope: Cell::new(None),
             })),
         }

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

@@ -19,6 +19,7 @@
 //! This module includes just the barebones for a complete VirtualDOM API.
 //! Additional functionality is defined in the respective files.
 
+use crate::hooks::{SuspendedContext, SuspenseHook};
 use crate::{arena::SharedResources, innerlude::*};
 
 use std::any::Any;

+ 2 - 1
packages/web/examples/async_web.rs

@@ -31,7 +31,8 @@ const ENDPOINT: &str = "https://dog.ceo/api/breeds/image/random/";
 static App: FC<()> = |cx| {
     let state = use_state(cx, || 0);
 
-    let dog_node = cx.use_suspense(
+    let dog_node = use_suspense(
+        cx,
         || surf::get(ENDPOINT).recv_json::<DogApi>(),
         |cx, res| match res {
             Ok(res) => rsx!(in cx, img { src: "{res.message}" }),