From e5c88fe3a49649ecb308decd14c2557963978619 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 27 Jul 2021 11:28:05 -0400 Subject: [PATCH] feat: make hooks free-functions --- README.md | 1 + examples/async.rs | 2 +- examples/coroutine.rs | 4 +- examples/reference/suspense.rs | 3 +- examples/reference/task.rs | 2 +- packages/core/examples/async.rs | 4 +- packages/core/examples/fragment_from_iter.rs | 3 +- packages/core/src/context.rs | 384 ++++--------------- packages/core/src/hooks.rs | 256 +++++++++++++ packages/core/src/lib.rs | 7 +- packages/core/src/nodes.rs | 26 +- packages/core/src/virtual_dom.rs | 1 + packages/web/examples/async_web.rs | 3 +- 13 files changed, 373 insertions(+), 323 deletions(-) create mode 100644 packages/core/src/hooks.rs diff --git a/README.md b/README.md index 793df7b5e..b65f9df8b 100644 --- a/README.md +++ b/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... diff --git a/examples/async.rs b/examples/async.rs index 69084c530..8e6766bd1 100644 --- a/examples/async.rs +++ b/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; diff --git a/examples/coroutine.rs b/examples/coroutine.rs index 7617cae7a..26386055c 100644 --- a/examples/coroutine.rs +++ b/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; diff --git a/examples/reference/suspense.rs b/examples/reference/suspense.rs index a86b6a407..d27572d30 100644 --- a/examples/reference/suspense.rs +++ b/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::(), |cx, res| match res { Ok(res) => rsx!(in cx, img { src: "{res.message}" }), diff --git a/examples/reference/task.rs b/examples/reference/task.rs index ac1fc3686..649c3d97a 100644 --- a/examples/reference/task.rs +++ b/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; diff --git a/packages/core/examples/async.rs b/packages/core/examples/async.rs index 87debae49..9c6463e8a 100644 --- a/packages/core/examples/async.rs +++ b/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!() }; diff --git a/packages/core/examples/fragment_from_iter.rs b/packages/core/examples/fragment_from_iter.rs index c559eabba..08018c68c 100644 --- a/packages/core/examples/fragment_from_iter.rs +++ b/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| { // diff --git a/packages/core/src/context.rs b/packages/core/src/context.rs index 753626d96..9519f2108 100644 --- a/packages/core/src/context.rs +++ b/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 { + pub fn prepare_update(&self) -> Rc { 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(self, val: T) -> Option> { + self.scope + .shared_contexts + .borrow_mut() + .insert(TypeId::of::(), Rc::new(val)) + } + + /// Walk the tree to find a shared state with the TypeId of the generic type + /// + pub fn consume_shared_state(self) -> Option> { + let mut scope = Some(self.scope); + let mut par = None; + + let ty = TypeId::of::(); + 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::() + .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::().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(self, init: F) -> &'src Rc - where - T: 'static, - F: FnOnce() -> T, - { - let ty = TypeId::of::(); - 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::() - } - - /// There are hooks going on here! - pub fn use_context(self) -> &'src Rc { - 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(self) -> Option<&'src Rc> { - struct UseContextHook { - par: Option>, - } - - self.use_hook( - move |_| { - let mut scope = Some(self.scope); - let mut par = None; - - let ty = TypeId::of::(); - 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::() - .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( - self, - task_initializer: Init, - ) -> (&'src TaskHandle, &'src Option) - where - Out: 'static, - Fut: Future + 'static, - Init: FnOnce() -> Fut + 'src, - { - struct TaskHook { - handle: TaskHandle, - task_dump: Rc>>, - value: Option, - } - - // 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>>>, - pub callback: SuspendedCallback, - pub dom_node_id: Rc>>, -} -type SuspendedCallback = Box 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( - self, - task_initializer: impl FnOnce() -> Fut, - user_callback: Cb, - ) -> DomTree<'src> - where - Fut: Future + '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>> = slot.as_ref().borrow(); - match v.as_ref() { - Some(a) => { - let v: &dyn Any = a.as_ref(); - let real_val = v.downcast_ref::().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); - 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) -> 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 })) - } } diff --git a/packages/core/src/hooks.rs b/packages/core/src/hooks.rs new file mode 100644 index 000000000..3add4a7f1 --- /dev/null +++ b/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 +where + T: 'static, + F: FnOnce() -> T, +{ + let ty = TypeId::of::(); + 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::(cx) +} + +/// There are hooks going on here! +pub fn use_consume_state<'src, T: 'static, P>(cx: Context<'src, P>) -> &'src Rc { + use_try_consume_state::(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> { + struct UseContextHook(Option>); + + cx.use_hook( + move |_| UseContextHook(cx.consume_shared_state::()), + 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) +where + Out: 'static, + Fut: Future + 'static, + Init: FnOnce() -> Fut + 'src, +{ + struct TaskHook { + handle: TaskHandle, + task_dump: Rc>>, + value: Option, + } + + // 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 + '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>> = slot.as_ref().borrow(); + match v.as_ref() { + Some(a) => { + let v: &dyn Any = a.as_ref(); + let real_val = v.downcast_ref::().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); + 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>>>, + pub callback: SuspendedCallback, + pub dom_node_id: Rc>>, +} +type SuspendedCallback = Box Fn(SuspendedContext<'a>) -> DomTree<'a>>; +pub struct SuspendedContext<'a> { + pub(crate) inner: Context<'a, ()>, +} +impl<'src> SuspendedContext<'src> { + pub fn render) -> 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 })) + } +} diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index fa9f4e221..14c9855ab 100644 --- a/packages/core/src/lib.rs +++ b/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; diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 3cbd6a35c..b8e68b7ef 100644 --- a/packages/core/src/nodes.rs +++ b/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

is the same by function pointer + // - Because FC

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), })), } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 3001e26ec..ef0a11694 100644 --- a/packages/core/src/virtual_dom.rs +++ b/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; diff --git a/packages/web/examples/async_web.rs b/packages/web/examples/async_web.rs index ebafe8c3f..6c1a8f439 100644 --- a/packages/web/examples/async_web.rs +++ b/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::(), |cx, res| match res { Ok(res) => rsx!(in cx, img { src: "{res.message}" }),