Fix clearing error boundaries (#4041)

This commit is contained in:
Evan Almloff authored and GitHub committed 2025-05-07 13:31:47 -07:00
1 parent 28a85ab614
commit 04b14c5abc
2 files changed
+129 -19

No files matched your search

+81 -18
View File
@@ -268,6 +268,7 @@ impl ErrorContext {
/// Clear all errors from this Error Boundary
pub fn clear_errors(&self) {
self.errors.borrow_mut().clear();
self.id.needs_update();
}
}
@@ -696,33 +697,46 @@ impl<
///
/// ## Details
///
/// Error boundaries handle errors within a specific part of your application. Any errors passed up from a child will be caught by the nearest error boundary.
/// Error boundaries handle errors within a specific part of your application. They are similar to `try/catch` in JavaScript, but they only catch errors in the tree below them.
/// Any errors passed up from a child will be caught by the nearest error boundary. Error boundaries are quick to implement, but it can be useful to individually handle errors
/// in your components to provide a better user experience when you know that an error is likely to occur.
///
/// ## Example
///
/// ```rust, no_run
/// # use dioxus::prelude::*;
/// use dioxus::prelude::*;
///
/// fn App() -> Element {
/// let mut multiplier = use_signal(|| String::from("2"));
/// rsx! {
/// input {
/// r#type: "text",
/// value: multiplier,
/// oninput: move |e| multiplier.set(e.value())
/// }
/// ErrorBoundary {
/// handle_error: |errors: ErrorContext| rsx! { "Oops, we encountered an error. Please report {errors:?} to the developer of this application" },
/// handle_error: |errors: ErrorContext| {
/// rsx! {
/// div {
/// "Oops, we encountered an error. Please report {errors:?} to the developer of this application"
/// }
/// }
/// },
/// Counter {
/// multiplier: "1234"
/// multiplier
/// }
/// }
/// }
/// }
///
/// #[component]
/// fn Counter(multiplier: String) -> Element {
/// // You can bubble up errors with `?` inside components
/// let multiplier_parsed = multiplier.parse::<usize>()?;
/// fn Counter(multiplier: ReadOnlySignal<String>) -> Element {
/// let multiplier_parsed = multiplier().parse::<usize>()?;
/// let mut count = use_signal(|| multiplier_parsed);
/// rsx! {
/// button {
/// // Or inside event handlers
/// onclick: move |_| {
/// let multiplier_parsed = multiplier.parse::<usize>()?;
/// let multiplier_parsed = multiplier().parse::<usize>()?;
/// *count.write() *= multiplier_parsed;
/// Ok(())
/// },
@@ -732,16 +746,69 @@ impl<
/// }
/// ```
///
/// ## Usage
/// ## Resetting the error boundary
///
/// Error boundaries are an easy way to handle errors in your application.
/// They are similar to `try/catch` in JavaScript, but they only catch errors in the tree below them.
/// Error boundaries are quick to implement, but it can be useful to individually handle errors in your components to provide a better user experience when you know that an error is likely to occur.
/// Once the error boundary catches an error, it will render the rsx returned from the handle_error function instead of the children. To reset the error boundary,
/// you can call the [`ErrorContext::clear_errors`] method. This will clear all errors and re-render the children.
///
/// ```rust, no_run
/// # use dioxus::prelude::*;
/// fn App() -> Element {
/// let mut multiplier = use_signal(|| String::new());
/// rsx! {
/// input {
/// r#type: "text",
/// value: multiplier,
/// oninput: move |e| multiplier.set(e.value())
/// }
/// ErrorBoundary {
/// handle_error: |errors: ErrorContext| {
/// rsx! {
/// div {
/// "Oops, we encountered an error. Please report {errors:?} to the developer of this application"
/// }
/// button {
/// onclick: move |_| {
/// errors.clear_errors();
/// },
/// "try again"
/// }
/// }
/// },
/// Counter {
/// multiplier
/// }
/// }
/// }
/// }
///
/// #[component]
/// fn Counter(multiplier: ReadOnlySignal<String>) -> Element {
/// let multiplier_parsed = multiplier().parse::<usize>()?;
/// let mut count = use_signal(|| multiplier_parsed);
/// rsx! {
/// button {
/// onclick: move |_| {
/// let multiplier_parsed = multiplier().parse::<usize>()?;
/// *count.write() *= multiplier_parsed;
/// Ok(())
/// },
/// "{count}x{multiplier}"
/// }
/// }
/// }
/// ```
#[allow(non_upper_case_globals, non_snake_case)]
pub fn ErrorBoundary(props: ErrorBoundaryProps) -> Element {
let error_boundary = use_hook(provide_error_boundary);
let errors = error_boundary.errors();
if errors.is_empty() {
let has_errors = !errors.is_empty();
// Drop errors before running user code that might borrow the error lock
drop(errors);
if has_errors {
(props.handle_error.0)(error_boundary.clone())
} else {
std::result::Result::Ok({
static TEMPLATE: Template = Template {
roots: &[TemplateNode::Dynamic { id: 0usize }],
@@ -755,9 +822,5 @@ pub fn ErrorBoundary(props: ErrorBoundaryProps) -> Element {
Default::default(),
)
})
} else {
tracing::trace!("scope id: {:?}", current_scope_id());
tracing::trace!("handling errors: {:?}", errors);
(props.handle_error.0)(error_boundary.clone())
}
}
+48 -1
View File
@@ -1,6 +1,6 @@
#![allow(non_snake_case)]
use dioxus::prelude::*;
use dioxus::{prelude::*, CapturedError};
#[test]
fn catches_panic() {
@@ -30,3 +30,50 @@ fn ThrowChild() -> Element {
rsx! { div {} }
}
#[test]
fn clear_error_boundary() {
static THREW_ERROR: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
#[component]
fn App() -> Element {
rsx! {
AutoClearError {}
}
}
#[component]
pub fn ThrowsError() -> Element {
if THREW_ERROR.load(std::sync::atomic::Ordering::SeqCst) {
THREW_ERROR.store(true, std::sync::atomic::Ordering::SeqCst);
Err(CapturedError::from_display("This is an error").into())
} else {
rsx! {
"We should see this"
}
}
}
#[component]
pub fn AutoClearError() -> Element {
rsx! {
ErrorBoundary {
handle_error: |error: ErrorContext| {
error.clear_errors();
rsx! {
"We cleared it"
}
},
ThrowsError {}
}
}
}
let mut dom = VirtualDom::new(App);
dom.rebuild(&mut dioxus_core::NoOpMutations);
let out = dioxus_ssr::render(&dom);
assert_eq!(out, "We should see this");
}