mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-10 06:34:20 +00:00
feat: make hooks free-functions
This commit is contained in:
parent
f782e14211
commit
e5c88fe3a4
13 changed files with 373 additions and 323 deletions
|
@ -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...
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}" }),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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!()
|
||||
};
|
||||
|
|
|
@ -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| {
|
||||
//
|
||||
|
|
|
@ -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
packages/core/src/hooks.rs
Normal file
256
packages/core/src/hooks.rs
Normal file
|
@ -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 }))
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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),
|
||||
})),
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}" }),
|
||||
|
|
Loading…
Reference in a new issue