diff --git a/packages/core/src/any_props.rs b/packages/core/src/any_props.rs index c3ea4dc2c..7014a2771 100644 --- a/packages/core/src/any_props.rs +++ b/packages/core/src/any_props.rs @@ -1,4 +1,4 @@ -use std::marker::PhantomData; +use std::{marker::PhantomData, panic::AssertUnwindSafe}; use crate::{ innerlude::Scoped, @@ -62,12 +62,19 @@ where } fn render(&'a self, cx: &'a ScopeState) -> RenderReturn<'a> { - let scope: &mut Scoped

= cx.bump().alloc(Scoped { - props: &self.props, - scope: cx, - }); + let res = std::panic::catch_unwind(AssertUnwindSafe(move || { + // Call the render function directly + let scope: &mut Scoped

= cx.bump().alloc(Scoped { + props: &self.props, + scope: cx, + }); - // Call the render function directly - (self.render_fn)(scope).into_return(cx) + (self.render_fn)(scope).into_return(cx) + })); + + match res { + Ok(e) => e, + Err(_) => RenderReturn::default(), + } } } diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index 9d723d89c..27b3a048a 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -81,12 +81,12 @@ impl VirtualDom { self.ensure_drop_safety(id); if let Some(root) = self.scopes[id.0].as_ref().try_root_node() { - if let RenderReturn::Sync(Some(node)) = unsafe { root.extend_lifetime_ref() } { + if let RenderReturn::Ready(node) = unsafe { root.extend_lifetime_ref() } { self.drop_scope_inner(node) } } if let Some(root) = unsafe { self.scopes[id.0].as_ref().previous_frame().try_load_node() } { - if let RenderReturn::Sync(Some(node)) = unsafe { root.extend_lifetime_ref() } { + if let RenderReturn::Ready(node) = unsafe { root.extend_lifetime_ref() } { self.drop_scope_inner(node) } } diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index 239bd85fa..b2be8e60e 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -354,8 +354,13 @@ impl<'b> VirtualDom { use RenderReturn::*; match return_nodes { - Sync(Some(t)) => self.mount_component(scope, template, t, idx), - Sync(None) => todo!("Propogate error upwards"), + Ready(t) => self.mount_component(scope, template, t, idx), + Aborted(t) => { + self.mutations + .push(Mutation::CreatePlaceholder { id: ElementId(999) }); + + 1 + } Async(_) => self.mount_component_placeholder(template, idx, scope), } } diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index abf938929..4112fdd55 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -30,29 +30,44 @@ impl<'b> VirtualDom { .try_load_node() .expect("Call rebuild before diffing"); - use RenderReturn::{Async, Sync}; + use RenderReturn::{Aborted, Async, Ready}; match (old, new) { - (Sync(Some(l)), Sync(Some(r))) => self.diff_node(l, r), + // Normal pathway + (Ready(l), Ready(r)) => self.diff_node(l, r), - // Err cases - (Sync(Some(l)), Sync(None)) => self.diff_ok_to_err(l), - (Sync(None), Sync(Some(r))) => self.diff_err_to_ok(r), - (Sync(None), Sync(None)) => { /* nothing */ } + // Unwind the mutations if need be + (Ready(l), Aborted(p)) => self.diff_ok_to_err(l, p), - // Async - (Sync(Some(_l)), Async(_)) => todo!(), - (Sync(None), Async(_)) => todo!(), - (Async(_), Sync(Some(_r))) => todo!(), - (Async(_), Sync(None)) => { /* nothing */ } - (Async(_), Async(_)) => { /* nothing */ } + // Just move over the placeholder + (Aborted(l), Aborted(r)) => r.id.set(l.id.get()), + + // Becomes async, do nothing while we ait + (Ready(nodes), Async(fut)) => todo!(), + + // Placeholder becomes something + // We should also clear the error now + (Aborted(l), Ready(r)) => self.replace_placeholder(l, [r]), + + (Aborted(_), Async(_)) => todo!("async should not resolve here"), + (Async(_), Ready(_)) => todo!("async should not resolve here"), + (Async(_), Aborted(_)) => todo!("async should not resolve here"), + (Async(_), Async(_)) => { + // All suspense should resolve before we diff it again + panic!("Should not roll from suspense to suspense."); + } }; } self.scope_stack.pop(); } - fn diff_ok_to_err(&mut self, _l: &'b VNode<'b>) {} - fn diff_err_to_ok(&mut self, _l: &'b VNode<'b>) {} + fn diff_ok_to_err(&mut self, _l: &'b VNode<'b>, _p: &'b VPlaceholder) { + todo!() + } + + fn diff_err_to_ok(&mut self, _l: &'b VNode<'b>) { + todo!("Dioxus cannot currently recover a component after it has been errored. It must be removed from a parent"); + } fn diff_node(&mut self, left_template: &'b VNode<'b>, right_template: &'b VNode<'b>) { // If the templates are the same, we don't need to do anything, nor do we want to @@ -118,7 +133,7 @@ impl<'b> VirtualDom { (Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right), (Placeholder(left), Placeholder(right)) => right.id.set(left.id.get()), (Component(left), Component(right)) => self.diff_vcomponent(left, right, node, idx), - (Placeholder(left), Fragment(right)) => self.replace_placeholder(left, right), + (Placeholder(left), Fragment(right)) => self.replace_placeholder(left, *right), (Fragment(left), Placeholder(right)) => self.node_to_placeholder(left, right), _ => todo!("This is an usual custom case for dynamic nodes. We don't know how to handle it yet."), }; @@ -679,7 +694,8 @@ impl<'b> VirtualDom { Component(comp) => { let scope = comp.scope.get().unwrap(); match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { - RenderReturn::Sync(Some(node)) => self.push_all_real_nodes(node), + RenderReturn::Ready(node) => self.push_all_real_nodes(node), + RenderReturn::Aborted(node) => todo!(), _ => todo!(), } } @@ -707,7 +723,11 @@ impl<'b> VirtualDom { } /// Simply replace a placeholder with a list of nodes - fn replace_placeholder(&mut self, l: &'b VPlaceholder, r: &'b [VNode<'b>]) { + fn replace_placeholder( + &mut self, + l: &'b VPlaceholder, + r: impl IntoIterator>, + ) { let m = self.create_children(r); let id = l.id.get().unwrap(); self.mutations.push(Mutation::ReplaceWith { id, m }); @@ -856,7 +876,7 @@ impl<'b> VirtualDom { let scope = comp.scope.take().unwrap(); match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { - RenderReturn::Sync(Some(t)) => { + RenderReturn::Ready(t) => { println!("Removing component node sync {:?}", gen_muts); self.remove_node(t, gen_muts) } @@ -886,7 +906,7 @@ impl<'b> VirtualDom { Some(Component(comp)) => { let scope = comp.scope.get().unwrap(); match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { - RenderReturn::Sync(Some(t)) => self.find_first_element(t), + RenderReturn::Ready(t) => self.find_first_element(t), _ => todo!("cannot handle nonstandard nodes"), } } @@ -902,7 +922,7 @@ impl<'b> VirtualDom { Some(Component(comp)) => { let scope = comp.scope.get().unwrap(); match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { - RenderReturn::Sync(Some(t)) => self.find_last_element(t), + RenderReturn::Ready(t) => self.find_last_element(t), _ => todo!("cannot handle nonstandard nodes"), } } diff --git a/packages/core/src/error_boundary.rs b/packages/core/src/error_boundary.rs index fd8810f1a..91ac3b61d 100644 --- a/packages/core/src/error_boundary.rs +++ b/packages/core/src/error_boundary.rs @@ -1,14 +1,37 @@ -use std::cell::RefCell; - -use crate::ScopeId; +use crate::{ScopeId, ScopeState}; +use std::{ + any::{Any, TypeId}, + cell::RefCell, + fmt::Debug, +}; /// A boundary that will capture any errors from child components -#[allow(dead_code)] pub struct ErrorBoundary { - error: RefCell>, + error: RefCell>, id: ScopeId, } +/// An instance of an error captured by a descendant component. +pub struct CapturedError { + /// The error captured by the error boundary + pub error: Box, + + /// The scope that threw the error + pub scope: ScopeId, +} + +impl CapturedError { + /// Downcast the error type into a concrete error type + pub fn downcast(&self) -> Option<&T> { + if TypeId::of::() == self.error.type_id() { + let raw = self.error.as_ref() as *const _ as *const T; + Some(unsafe { &*raw }) + } else { + None + } + } +} + impl ErrorBoundary { pub fn new(id: ScopeId) -> Self { Self { @@ -16,4 +39,104 @@ impl ErrorBoundary { id, } } + + /// Push an error into this Error Boundary + pub fn insert_error(&self, scope: ScopeId, error: Box) { + self.error.replace(Some(CapturedError { error, scope })); + } +} + +/// A trait to allow results to be thrown upwards to the nearest Error Boundary +/// +/// The canonical way of using this trait is to throw results from hooks, aborting rendering +/// through question mark synax. The throw method returns an option that evalutes to None +/// if there is an error, injecting the error to the nearest error boundary. +/// +/// If the value is `Ok`, then throw returns the value, not aborting the rendering preocess. +/// +/// The call stack is saved for this component and provided to the error boundary +/// +/// ```rust, ignore +/// +/// #[inline_props] +/// fn app(cx: Scope, count: String) -> Element { +/// let id: i32 = count.parse().throw(cx)?; +/// +/// cx.render(rsx! { +/// div { "Count {}" } +/// }) +/// } +/// ``` +pub trait Throw { + /// The value that will be returned in if the given value is `Ok`. + type Out; + + /// Returns an option that evalutes to None if there is an error, injecting the error to the nearest error boundary. + /// + /// If the value is `Ok`, then throw returns the value, not aborting the rendering preocess. + /// + /// The call stack is saved for this component and provided to the error boundary + /// + /// + /// Note that you can also manually throw errors using the throw method on `ScopeState` directly, + /// which is what this trait shells out to. + /// + /// + /// ```rust, ignore + /// + /// #[inline_props] + /// fn app(cx: Scope, count: String) -> Element { + /// let id: i32 = count.parse().throw(cx)?; + /// + /// cx.render(rsx! { + /// div { "Count {}" } + /// }) + /// } + /// ``` + fn throw(self, cx: &ScopeState) -> Option; +} + +/// We call clone on any errors that can be owned out of a reference +impl<'a, T, O: Debug + 'static, E: ToOwned> Throw for &'a Result { + type Out = &'a T; + + fn throw(self, cx: &ScopeState) -> Option { + match self { + Ok(t) => Some(t), + Err(e) => { + cx.throw(e.to_owned()); + None + } + } + } +} + +/// Or just throw errors we know about +impl Throw for Result { + type Out = T; + + fn throw(self, cx: &ScopeState) -> Option { + match self { + Ok(t) => Some(t), + Err(e) => { + cx.throw(e); + None + } + } + } +} + +/// Or just throw errors we know about +impl Throw for Option { + type Out = T; + + fn throw(self, cx: &ScopeState) -> Option { + match self { + Some(t) => Some(t), + None => { + cx.throw("None error."); + None + } + } + } } diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index dcf7d7862..f13c20b43 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -70,10 +70,10 @@ pub(crate) mod innerlude { } pub use crate::innerlude::{ - fc_to_builder, Attribute, AttributeValue, Component, DynamicNode, Element, ElementId, Event, - Fragment, IntoDynNode, LazyNodes, Mutation, Mutations, Properties, RenderReturn, Scope, - ScopeId, ScopeState, Scoped, SuspenseContext, TaskId, Template, TemplateAttribute, - TemplateNode, VComponent, VNode, VText, VirtualDom, + fc_to_builder, Attribute, AttributeValue, CapturedError, Component, DynamicNode, Element, + ElementId, Event, Fragment, IntoDynNode, LazyNodes, Mutation, Mutations, Properties, + RenderReturn, Scope, ScopeId, ScopeState, Scoped, SuspenseContext, TaskId, Template, + TemplateAttribute, TemplateNode, VComponent, VNode, VText, VirtualDom, }; /// The purpose of this module is to alleviate imports of many common types @@ -83,7 +83,7 @@ pub mod prelude { pub use crate::innerlude::{ fc_to_builder, Component, Element, Event, EventHandler, Fragment, LazyNodes, Properties, Scope, ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, - VNode, VirtualDom, + Throw, VNode, VirtualDom, }; } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 99b7499c9..e7c8c0e13 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -21,12 +21,26 @@ pub type TemplateId = &'static str; /// you might need to handle the case where there's no node immediately ready. pub enum RenderReturn<'a> { /// A currently-available element - Sync(Element<'a>), + Ready(VNode<'a>), + + /// The component aborted rendering early. It might've thrown an error. + /// + /// In its place we've produced a placeholder to locate its spot in the dom when + /// it recovers. + /// + /// The old nodes are kept around + Aborted(VPlaceholder), /// An ongoing future that will resolve to a [`Element`] Async(BumpBox<'a, dyn Future> + 'a>), } +impl<'a> Default for RenderReturn<'a> { + fn default() -> Self { + RenderReturn::Aborted(VPlaceholder::default()) + } +} + /// A reference to a template along with any context needed to hydrate it /// /// The dynamic parts of the template are stored separately from the static parts. This allows faster diffing by skipping @@ -416,7 +430,10 @@ pub trait ComponentReturn<'a, A = ()> { impl<'a> ComponentReturn<'a> for Element<'a> { fn into_return(self, _cx: &ScopeState) -> RenderReturn<'a> { - RenderReturn::Sync(self) + match self { + Some(node) => RenderReturn::Ready(node), + None => RenderReturn::Aborted(VPlaceholder::default()), + } } } diff --git a/packages/core/src/scheduler/mod.rs b/packages/core/src/scheduler/mod.rs index a6442184d..7aed417b1 100644 --- a/packages/core/src/scheduler/mod.rs +++ b/packages/core/src/scheduler/mod.rs @@ -8,7 +8,7 @@ mod waker; pub use suspense::*; pub use task::*; -pub use waker::RcWake; +pub use waker::ArcWake; /// The type of message that can be sent to the scheduler. /// @@ -25,16 +25,16 @@ pub(crate) enum SchedulerMsg { SuspenseNotified(SuspenseId), } -use std::{cell::RefCell, rc::Rc}; +use std::{cell::RefCell, rc::Rc, sync::Arc}; pub(crate) struct Scheduler { pub sender: futures_channel::mpsc::UnboundedSender, /// Tasks created with cx.spawn - pub tasks: RefCell>>, + pub tasks: RefCell>>, /// Async components - pub leaves: RefCell>>, + pub leaves: RefCell>>, } impl Scheduler { diff --git a/packages/core/src/scheduler/suspense.rs b/packages/core/src/scheduler/suspense.rs index 8d7d7337e..77d4671e7 100644 --- a/packages/core/src/scheduler/suspense.rs +++ b/packages/core/src/scheduler/suspense.rs @@ -1,7 +1,8 @@ -use super::{waker::RcWake, SchedulerMsg}; +use super::{waker::ArcWake, SchedulerMsg}; use crate::ElementId; use crate::{innerlude::Mutations, Element, ScopeId}; use std::future::Future; +use std::sync::Arc; use std::{ cell::{Cell, RefCell}, collections::HashSet, @@ -42,8 +43,8 @@ pub(crate) struct SuspenseLeaf { pub(crate) task: *mut dyn Future>, } -impl RcWake for SuspenseLeaf { - fn wake_by_ref(arc_self: &Rc) { +impl ArcWake for SuspenseLeaf { + fn wake_by_ref(arc_self: &Arc) { arc_self.notified.set(true); _ = arc_self .tx diff --git a/packages/core/src/scheduler/task.rs b/packages/core/src/scheduler/task.rs index fb4378104..7fa13ce36 100644 --- a/packages/core/src/scheduler/task.rs +++ b/packages/core/src/scheduler/task.rs @@ -1,7 +1,8 @@ -use super::{waker::RcWake, Scheduler, SchedulerMsg}; +use super::{waker::ArcWake, Scheduler, SchedulerMsg}; use crate::ScopeId; use std::cell::RefCell; use std::future::Future; +use std::sync::Arc; use std::{pin::Pin, rc::Rc}; /// A task's unique identifier. @@ -35,7 +36,7 @@ impl Scheduler { let entry = tasks.vacant_entry(); let task_id = TaskId(entry.key()); - entry.insert(Rc::new(LocalTask { + entry.insert(Arc::new(LocalTask { id: task_id, tx: self.sender.clone(), task: RefCell::new(Box::pin(task)), @@ -57,8 +58,8 @@ impl Scheduler { } } -impl RcWake for LocalTask { - fn wake_by_ref(arc_self: &Rc) { +impl ArcWake for LocalTask { + fn wake_by_ref(arc_self: &Arc) { _ = arc_self .tx .unbounded_send(SchedulerMsg::TaskNotified(arc_self.id)); diff --git a/packages/core/src/scheduler/wait.rs b/packages/core/src/scheduler/wait.rs index 8dff17b01..205bd8ea0 100644 --- a/packages/core/src/scheduler/wait.rs +++ b/packages/core/src/scheduler/wait.rs @@ -10,7 +10,7 @@ use crate::{ ScopeId, TaskId, VNode, VirtualDom, }; -use super::{waker::RcWake, SuspenseId}; +use super::{waker::ArcWake, SuspenseId}; impl VirtualDom { /// Handle notifications by tasks inside the scheduler @@ -67,18 +67,21 @@ impl VirtualDom { // we should attach them to that component and then render its children // continue rendering the tree until we hit yet another suspended component if let Poll::Ready(new_nodes) = as_pinned_mut.poll_unpin(&mut cx) { - // safety: we're not going to modify the suspense context but we don't want to make a clone of it let fiber = self.acquire_suspense_boundary(leaf.scope_id); let scope = &mut self.scopes[scope_id.0]; let arena = scope.current_frame(); - let ret = arena.bump.alloc(RenderReturn::Sync(new_nodes)); + let ret = arena.bump.alloc(match new_nodes { + Some(new) => RenderReturn::Ready(new), + None => RenderReturn::default(), + }); + arena.node.set(ret); fiber.waiting_on.borrow_mut().remove(&id); - if let RenderReturn::Sync(Some(template)) = ret { + if let RenderReturn::Ready(template) = ret { let mutations_ref = &mut fiber.mutations.borrow_mut(); let mutations = &mut **mutations_ref; let template: &VNode = unsafe { std::mem::transmute(template) }; diff --git a/packages/core/src/scheduler/waker.rs b/packages/core/src/scheduler/waker.rs index 762020167..fc75fe8fd 100644 --- a/packages/core/src/scheduler/waker.rs +++ b/packages/core/src/scheduler/waker.rs @@ -1,36 +1,37 @@ +use std::mem; +use std::sync::Arc; use std::task::{RawWaker, RawWakerVTable, Waker}; -use std::{mem, rc::Rc}; -pub trait RcWake: Sized { +pub trait ArcWake: Sized { /// Create a waker from this self-wakening object - fn waker(self: &Rc) -> Waker { - unsafe fn rc_vtable() -> &'static RawWakerVTable { + fn waker(self: &Arc) -> Waker { + unsafe fn rc_vtable() -> &'static RawWakerVTable { &RawWakerVTable::new( |data| { - let arc = mem::ManuallyDrop::new(Rc::::from_raw(data.cast::())); + let arc = mem::ManuallyDrop::new(Arc::::from_raw(data.cast::())); let _rc_clone: mem::ManuallyDrop<_> = arc.clone(); RawWaker::new(data, rc_vtable::()) }, - |data| Rc::from_raw(data.cast::()).wake(), + |data| Arc::from_raw(data.cast::()).wake(), |data| { - let arc = mem::ManuallyDrop::new(Rc::::from_raw(data.cast::())); - RcWake::wake_by_ref(&arc); + let arc = mem::ManuallyDrop::new(Arc::::from_raw(data.cast::())); + ArcWake::wake_by_ref(&arc); }, - |data| drop(Rc::::from_raw(data.cast::())), + |data| drop(Arc::::from_raw(data.cast::())), ) } unsafe { Waker::from_raw(RawWaker::new( - Rc::into_raw(self.clone()).cast(), + Arc::into_raw(self.clone()).cast(), rc_vtable::(), )) } } - fn wake_by_ref(arc_self: &Rc); + fn wake_by_ref(arc_self: &Arc); - fn wake(self: Rc) { + fn wake(self: Arc) { Self::wake_by_ref(&self) } } diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index 9c0f96db3..dc9158520 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -1,10 +1,10 @@ use crate::{ any_props::AnyProps, bump_frame::BumpFrame, - innerlude::DirtyScope, + innerlude::{DirtyScope, VPlaceholder}, innerlude::{SuspenseId, SuspenseLeaf}, nodes::RenderReturn, - scheduler::RcWake, + scheduler::ArcWake, scopes::{ScopeId, ScopeState}, virtual_dom::VirtualDom, }; @@ -14,6 +14,7 @@ use std::{ mem, pin::Pin, rc::Rc, + sync::Arc, task::{Context, Poll}, }; @@ -79,6 +80,7 @@ impl VirtualDom { // safety: due to how we traverse the tree, we know that the scope is not currently aliased let props: &dyn AnyProps = scope.props.as_ref().unwrap().as_ref(); let props: &dyn AnyProps = mem::transmute(props); + props.render(scope).extend_lifetime() }; @@ -89,7 +91,7 @@ impl VirtualDom { let entry = leaves.vacant_entry(); let suspense_id = SuspenseId(entry.key()); - let leaf = Rc::new(SuspenseLeaf { + let leaf = Arc::new(SuspenseLeaf { scope_id, task: task.as_mut(), id: suspense_id, @@ -108,7 +110,11 @@ impl VirtualDom { match pinned.poll_unpin(&mut cx) { // If nodes are produced, then set it and we can break Poll::Ready(nodes) => { - new_nodes = RenderReturn::Sync(nodes); + new_nodes = match nodes { + Some(nodes) => RenderReturn::Ready(nodes), + None => RenderReturn::default(), + }; + break; } @@ -150,6 +156,6 @@ impl VirtualDom { }); // rebind the lifetime now that its stored internally - unsafe { mem::transmute(allocated) } + unsafe { allocated.extend_lifetime_ref() } } } diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 6ceaa5eae..f6c81db28 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -4,7 +4,7 @@ use crate::{ arena::ElementId, bump_frame::BumpFrame, innerlude::{DynamicNode, EventHandler, VComponent, VText}, - innerlude::{Scheduler, SchedulerMsg}, + innerlude::{ErrorBoundary, Scheduler, SchedulerMsg}, lazynodes::LazyNodes, nodes::{ComponentReturn, IntoAttributeValue, IntoDynNode, RenderReturn}, Attribute, AttributeValue, Element, Event, Properties, TaskId, @@ -14,7 +14,7 @@ use rustc_hash::{FxHashMap, FxHashSet}; use std::{ any::{Any, TypeId}, cell::{Cell, RefCell}, - fmt::Arguments, + fmt::{Arguments, Debug}, future::Future, rc::Rc, sync::Arc, @@ -509,6 +509,19 @@ impl<'src> ScopeState { AttributeValue::Listener(RefCell::new(Some(boxed))) } + /// Inject an error into the nearest error boundary and quit rendering + /// + /// The error doesn't need to implement Error or any specific traits since the boundary + /// itself will downcast the error into a trait object. + pub fn throw(&self, error: impl Debug + 'static) -> Option<()> { + if let Some(cx) = self.consume_context::>() { + cx.insert_error(self.scope_id(), Box::new(error)); + } + + // Always return none during a throw + None + } + /// Store a value between renders. The foundational hook for all other hooks. /// /// Accepts an `initializer` closure, which is run on the first use of the hook (typically the initial render). The return value of this closure is stored for the lifetime of the component, and a mutable reference to it is provided on every render as the return value of `use_hook`. diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 217768c50..0940ea506 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -477,7 +477,7 @@ impl VirtualDom { pub fn rebuild(&mut self) -> Mutations { match unsafe { self.run_scope(ScopeId(0)).extend_lifetime_ref() } { // Rebuilding implies we append the created elements to the root - RenderReturn::Sync(Some(node)) => { + RenderReturn::Ready(node) => { let m = self.create_scope(ScopeId(0), node); self.mutations.edits.push(Mutation::AppendChildren { id: ElementId(0), @@ -485,7 +485,7 @@ impl VirtualDom { }); } // If an error occurs, we should try to render the default error component and context where the error occured - RenderReturn::Sync(None) => panic!("Cannot catch errors during rebuild"), + RenderReturn::Aborted(placeholder) => panic!("Cannot catch errors during rebuild"), RenderReturn::Async(_) => unreachable!("Root scope cannot be an async component"), } diff --git a/packages/core/tests/error_boundary.rs b/packages/core/tests/error_boundary.rs new file mode 100644 index 000000000..7cc1122cd --- /dev/null +++ b/packages/core/tests/error_boundary.rs @@ -0,0 +1,50 @@ +use dioxus::prelude::*; +use futures_util::Future; + +#[test] +fn catches_panic() { + let mut dom = VirtualDom::new(app); + + let a = dom.rebuild(); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + div { + PanicChild {} + } + }) +} + +fn PanicChild(cx: Scope) -> Element { + panic!("Rendering panicked for whatever reason"); + + cx.render(rsx! { + h1 { "It works!" } + }) +} + +fn ThrowChild(cx: Scope) -> Element { + cx.throw(std::io::Error::new(std::io::ErrorKind::AddrInUse, "asd"))?; + + let g: i32 = "123123".parse().throw(cx)?; + + cx.render(rsx! { + div {} + }) +} + +fn custom_allocator(cx: Scope) -> Element { + let g = String::new(); + + let p = g.as_str(); + + let g2 = cx.use_hook(|| 123); + // cx.spawn(async move { + + // // + // // println!("Thig is {p}"); + // }); + + None +}