use crate::innerlude::*; use futures_util::FutureExt; use std::{ any::{Any, TypeId}, cell::{Cell, RefCell}, future::Future, marker::PhantomData, ops::Deref, rc::{Rc, Weak}, }; /// Components in Dioxus use the "Context" object to interact with their lifecycle. /// This lets components schedule updates, integrate hooks, and expose their context via the context api. /// /// Properties passed down from the parent component are also directly accessible via the exposed "props" field. /// /// ```ignore /// #[derive(Properties)] /// struct Props { /// name: String /// /// } /// /// fn example(cx: Context) -> VNode { /// html! { ///
"Hello, {cx.name}"
/// } /// } /// ``` /// /// ## Available Methods: /// - render /// - use_hook /// - use_task /// - use_suspense /// - submit_task /// - children /// - use_effect /// pub struct Context<'src, T> { pub props: &'src T, pub scope: &'src Scope, } impl<'src, T> Copy for Context<'src, T> {} impl<'src, T> Clone for Context<'src, T> { fn clone(&self) -> Self { Self { props: self.props, scope: self.scope, } } } // We currently deref to props, but it might make more sense to deref to Scope? // This allows for code that takes cx.xyz instead of cx.props.xyz impl<'a, T> Deref for Context<'a, T> { type Target = &'a T; fn deref(&self) -> &Self::Target { &self.props } } impl<'src, P> Context<'src, P> { /// Access the children elements passed into the component /// /// This enables patterns where a component is passed children from its parent. /// /// ## Details /// /// Unlike React, Dioxus allows *only* lists of children to be passed from parent to child - not arbitrary functions /// or classes. If you want to generate nodes instead of accepting them as a list, consider declaring a closure /// on the props that takes Context. /// /// If a parent passes children into a component, the child will always re-render when the parent re-renders. In other /// words, a component cannot be automatically memoized if it borrows nodes from its parent, even if the component's /// props are valid for the static lifetime. /// /// ## Example /// /// ```rust /// const App: FC<()> = |cx| { /// cx.render(rsx!{ /// CustomCard { /// h1 {} /// p {} /// } /// }) /// } /// /// const CustomCard: FC<()> = |cx| { /// cx.render(rsx!{ /// div { /// h1 {"Title card"} /// {cx.children()} /// } /// }) /// } /// ``` pub fn children(&self) -> &'src [VNode<'src>] { self.scope.child_nodes } /// Create a subscription that schedules a future render for the reference component pub fn schedule_update(&self) -> Rc { self.scope.event_channel.clone() } pub fn schedule_effect(&self) -> Rc { todo!() } pub fn schedule_layout_effect(&self) { todo!() } /// Take a lazy VNode structure and actually build it with the context of the VDom's efficient VNode allocator. /// /// This function consumes the context and absorb the lifetime, so these VNodes *must* be returned. /// /// ## Example /// /// ```ignore /// fn Component(cx: Context<()>) -> VNode { /// // Lazy assemble the VNode tree /// let lazy_tree = html! {
"Hello World"
}; /// /// // Actually build the tree and allocate it /// cx.render(lazy_tree) /// } ///``` pub fn render) -> VNode<'src>>( self, lazy_nodes: LazyNodes<'src, F>, ) -> VNode<'src> { let scope_ref = self.scope; let listener_id = &scope_ref.listener_idx; lazy_nodes.into_vnode(NodeFactory { scope_ref, listener_id, }) } /// Store a value between renders /// /// - 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 /// /// ```ignore /// // use_ref is the simplest way of storing a value between renders /// pub fn use_ref(initial_value: impl FnOnce() -> T + 'static) -> Rc> { /// use_hook( /// || Rc::new(RefCell::new(initial_value())), /// |state| state.clone(), /// |_| {}, /// ) /// } /// ``` pub fn use_hook( self, initializer: Init, runner: Run, _cleanup: Cleanup, ) -> Output where State: 'static, Output: 'src, Init: FnOnce(usize) -> State, Run: FnOnce(&'src mut State) -> Output, Cleanup: FnOnce(State), { // If the idx is the same as the hook length, then we need to add the current hook if self.scope.hooks.at_end() { let new_state = initializer(self.scope.hooks.len()); self.scope.hooks.push(new_state); } const ERR_MSG: &str = r###" Unable to retrive the hook that was initialized in this index. Consult the `rules of hooks` to understand how to use hooks properly. You likely used the hook in a conditional. Hooks rely on consistent ordering between renders. 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_create_context(&self, init: impl Fn() -> T) { let mut cxs = self.scope.shared_contexts.borrow_mut(); let ty = TypeId::of::(); let is_initialized = self.use_hook( |_| false, |s| { let i = s.clone(); *s = true; i }, |_| {}, ); match (is_initialized, cxs.contains_key(&ty)) { // Do nothing, already initialized and already exists (true, true) => {} // Needs to be initialized (false, false) => { log::debug!("Initializing context..."); cxs.insert(ty, Rc::new(init())); } _ => debug_assert!(false, "Cannot initialize two contexts of the same type"), } } /// 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 pub fn try_use_context(self) -> Result<&'src Rc> { struct UseContextHook { par: Option>, we: Option>, } self.use_hook( move |_| UseContextHook { par: None as Option>, we: None as Option>, }, move |hook| { let mut scope = Some(self.scope); if let Some(we) = &hook.we { if let Some(re) = we.upgrade() { hook.par = Some(re); return Ok(hook.par.as_ref().unwrap()); } } let ty = TypeId::of::(); while let Some(inner) = scope { log::debug!( "Searching {:#?} for valid shared_context", inner.our_arena_idx ); let shared_contexts = inner.shared_contexts.borrow(); if let Some(shared_cx) = shared_contexts.get(&ty) { log::debug!("found matching cx"); let rc = shared_cx .clone() .downcast::() .expect("Should not fail, already validated the type from the hashmap"); hook.we = Some(Rc::downgrade(&rc)); hook.par = Some(rc); return Ok(hook.par.as_ref().unwrap()); } else { match inner.parent_idx { Some(parent_id) => { let parent = inner .arena_link .try_get(parent_id) .map_err(|_| Error::FatalInternal("Failed to find parent"))?; scope = Some(parent); } None => return Err(Error::MissingSharedContext), } } } Err(Error::MissingSharedContext) }, |_| {}, ) } /// `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.task_submitter)(task); TaskHandle { _p: PhantomData {} } } /// Awaits the given task, forcing the component to re-render when the value is ready. /// /// /// /// pub fn use_task(&self, task_initializer: Init) -> (&TaskHandle, &Option) where Out: 'static, Fut: Future + 'static, Init: FnOnce() -> Fut + 'src, { struct TaskHook { 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 update = self.schedule_update(); let originator = self.scope.our_arena_idx.clone(); self.submit_task(Box::pin(task_fut.then(move |output| async move { *slot.as_ref().borrow_mut() = Some(output); update(); EventTrigger { event: VirtualEvent::AsyncEvent { hook_idx }, originator, priority: EventPriority::Low, real_node_id: None, } }))); TaskHook { task_dump, value: None, } }, |hook| { if let Some(val) = hook.task_dump.as_ref().borrow_mut().take() { hook.value = Some(val); } (&TaskHandle { _p: PhantomData }, &hook.value) }, |_| {}, ) } } pub(crate) struct SuspenseHook { pub value: Rc>>>, pub callback: SuspendedCallback, pub dom_node_id: Rc>, } type SuspendedCallback = Box Fn(Context<'a, ()>) -> VNode<'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, ) -> VNode<'src> where Fut: Future + 'static, Out: 'static, Cb: for<'a> Fn(Context<'a, ()>, &Out) -> VNode<'a> + 'static, { self.use_hook( move |hook_idx| { let value = Rc::new(RefCell::new(None)); let dom_node_id = Rc::new(RealDomNode::empty_cell()); let domnode = dom_node_id.clone(); let slot = value.clone(); let callback: SuspendedCallback = Box::new(move |ctx: Context<()>| { 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 => { // VNode { dom_id: RealDomNode::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: &(), }; (&hook.callback)(cx) }, |_| {}, ) } } pub struct TaskHandle<'src> { _p: PhantomData<&'src ()>, }