Files
dioxus/packages/core/src/hooks.rs
T

272 lines
7.6 KiB
Rust

//! 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 |_| {
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 {
should_rerender: false,
},
scope: originator,
priority: EventPriority::Low,
mounted_dom_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::Suspended(VSuspended {
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 },
scope: originator,
priority: EventPriority::Low,
mounted_dom_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;
let bump = &self.inner.scope.frames.wip_frame().bump;
Some(lazy_nodes.into_vnode(NodeFactory { bump }))
}
}
#[derive(Clone, Copy)]
pub struct NodeRef<'src, T: 'static>(&'src RefCell<T>);
pub fn use_node_ref<T, P>(cx: Context<P>) -> NodeRef<T> {
cx.use_hook(
|f| {},
|f| {
//
todo!()
},
|f| {
//
},
)
}