Преглед на файлове

Fix clearing error boundaries (#4041)

Evan Almloff преди 1 месец
родител
ревизия
04b14c5abc
променени са 2 файла, в които са добавени 129 реда и са изтрити 19 реда
  1. 81 18
      packages/core/src/error_boundary.rs
  2. 48 1
      packages/core/tests/error_boundary.rs

+ 81 - 18
packages/core/src/error_boundary.rs

@@ -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
+///
+/// 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
+///             }
+///         }
+///     }
+/// }
 ///
-/// 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.
+/// #[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
packages/core/tests/error_boundary.rs

@@ -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");
+}