Pārlūkot izejas kodu

general cleanup

Evan Almloff 2 gadi atpakaļ
vecāks
revīzija
ece8f0fb22

+ 4 - 0
packages/router/examples/simple_routes.rs

@@ -101,6 +101,10 @@ fn Route3(cx: Scope, dynamic: String) -> Element {
             target: Route::Route2 { user_id: 8888 },
             "hello world link"
         }
+        button {
+            onclick: move |_| { router.push(NavigationTarget::External("https://www.google.com".to_string())); },
+            "google link"
+        }
         p { "Site Map" }
         pre { "{site_map:#?}" }
         p { "Dynamic link" }

+ 7 - 55
packages/router/src/components/default_errors.rs

@@ -1,15 +1,10 @@
-use crate::{
-    components::GenericLink, hooks::use_generic_route, navigation::NavigationTarget,
-    routable::Routable,
-};
+use crate::{hooks::use_generic_router, routable::Routable};
 use dioxus::prelude::*;
 
+/// The default component to render when an external navigation fails.
 #[allow(non_snake_case)]
 pub fn FailureExternalNavigation<R: Routable + Clone>(cx: Scope) -> Element {
-    let href = use_generic_route::<R>(cx).expect(
-        "`FailureExternalNavigation` can only be mounted by the router itself, \
-            since it is not exposed",
-    );
+    let router = use_generic_router::<R>(cx);
 
     render! {
         h1 { "External Navigation Failure!" }
@@ -18,53 +13,10 @@ pub fn FailureExternalNavigation<R: Routable + Clone>(cx: Scope) -> Element {
             "operation has failed. Click the link below to complete the navigation manually."
         }
         a {
-            href: "{href}",
-            rel: "noopener noreferrer",
-            "Click here to fix the failure."
-        }
-    }
-}
-
-#[allow(non_snake_case)]
-pub fn FailureNamedNavigation<R: Routable + Clone>(cx: Scope) -> Element {
-    render! {
-        h1 { "Named Navigation Failure!" }
-        p {
-            "The application has tried to navigate to an unknown name. This is a bug. Please "
-            "inform the developer, so they can fix it."
-            b { "Thank you!" }
-        }
-        p {
-            "We are sorry for the inconvenience. The link below may help to fix the problem, but "
-            "there is no guarantee."
-        }
-        GenericLink::<R> {
-            target: NavigationTarget::Internal(R::from_str("/").unwrap_or_else(|_| {
-                panic!("Failed to parse `/` as a Route")
-            })),
-            "Click here to try to fix the failure."
-        }
-    }
-}
-
-#[allow(non_snake_case)]
-pub fn FailureRedirectionLimit<R: Routable + Clone>(cx: Scope) -> Element {
-    render! {
-        h1 { "Redirection Limit Failure!" }
-        p {
-            "The application seems to have entered into an endless redirection loop. This is a "
-            "bug. Please inform the developer, so they can fix it."
-            b { "Thank you!" }
-        }
-        p {
-            "We are sorry for the inconvenience. The link below may help to fix the problem, but "
-            "there is no guarantee."
-        }
-        GenericLink::<R> {
-            target: NavigationTarget::Internal(R::from_str("/").unwrap_or_else(|_| {
-                panic!("Failed to parse `/` as a Route")
-            })),
-            "Click here to try to fix the failure."
+            onclick: move |_| {
+                router.clear_error()
+            },
+            "Click here to go back"
         }
     }
 }

+ 3 - 3
packages/router/src/components/outlet.rs

@@ -1,15 +1,15 @@
-use crate::{contexts::outlet::OutletContext, routable::Routable};
+use crate::prelude::{outlet::OutletContext, *};
 use dioxus::prelude::*;
 
 /// An outlet for the current content.
 ///
 /// Only works as descendant of a [`GenericRouter`] component, otherwise it will be inactive.
 ///
-/// The [`Outlet`] is aware of how many [`Outlet`]s it is nested within. It will render the content
+/// The [`GenericOutlet`] is aware of how many [`Outlet`]s it is nested within. It will render the content
 /// of the active route that is __exactly as deep__.
 ///
 /// # Panic
-/// - When the [`Outlet`] is not nested within another component calling the [`use_router`] hook,
+/// - When the [`GenericOutlet`] is not nested a [`GenericRouter`] component,
 ///   but only in debug builds.
 ///
 /// # Example

+ 2 - 2
packages/router/src/components/router.rs

@@ -9,7 +9,7 @@ use crate::{
     router_cfg::RouterConfiguration,
 };
 
-/// The config for [`Router`].
+/// The config for [`GenericRouter`].
 pub struct RouterCfg<R: Routable> {
     config: RefCell<Option<RouterConfiguration<R>>>,
 }
@@ -33,7 +33,7 @@ impl<R: Routable> From<RouterConfiguration<R>> for RouterCfg<R> {
     }
 }
 
-/// The props for [`Router`].
+/// The props for [`GenericRouter`].
 #[derive(Props)]
 pub struct GenericRouterProps<R: Routable + Serialize + DeserializeOwned>
 where

+ 14 - 5
packages/router/src/contexts/outlet.rs

@@ -1,6 +1,6 @@
 use dioxus::prelude::*;
 
-use crate::{hooks::use_generic_route, routable::Routable};
+use crate::{routable::Routable, utils::use_router_internal::use_router_internal};
 
 #[derive(Clone)]
 pub(crate) struct OutletContext {
@@ -16,7 +16,10 @@ pub(crate) fn use_outlet_context(cx: &ScopeState) -> &OutletContext {
 }
 
 impl OutletContext {
-    pub(crate) fn render<R: Routable + Clone>(cx: &ScopeState) -> Element<'_> {
+    pub(crate) fn render<R: Routable + Clone>(cx: Scope) -> Element<'_> {
+        let router = use_router_internal::<R>(cx)
+            .as_ref()
+            .expect("Outlet must be inside of a router");
         let outlet = use_outlet_context(cx);
         let current_level = outlet.current_level;
         cx.provide_context({
@@ -25,8 +28,14 @@ impl OutletContext {
             }
         });
 
-        use_generic_route::<R>(cx)
-            .expect("Outlet must be inside of a router")
-            .render(cx, current_level)
+        if let Some(error) = router.render_error(cx) {
+            if current_level == 0 {
+                return Some(error);
+            } else {
+                return None;
+            }
+        }
+
+        router.current().render(cx, current_level)
     }
 }

+ 78 - 640
packages/router/src/contexts/router.rs

@@ -11,12 +11,8 @@ use crate::{
 };
 
 /// An error that can occur when navigating.
-pub enum NavigationFailure<R: Routable> {
-    /// The router failed to navigate to an external URL.
-    External(String),
-    /// The router failed to navigate to an internal URL.
-    Internal(<R as std::str::FromStr>::Err),
-}
+#[derive(Debug, Clone)]
+pub struct ExternalNavigationFailure(String);
 
 /// A function the router will call after every routing update.
 pub type RoutingCallback<R> = Arc<dyn Fn(GenericRouterContext<R>) -> Option<NavigationTarget<R>>>;
@@ -40,6 +36,8 @@ where
     prefix: Option<String>,
 
     history: Box<dyn HistoryProvider<R>>,
+
+    unresolved_error: Option<ExternalNavigationFailure>,
 }
 
 /// A collection of router data that manages all routing functionality.
@@ -54,8 +52,6 @@ where
     routing_callback: Option<RoutingCallback<R>>,
 
     failure_external_navigation: fn(Scope) -> Element,
-    failure_named_navigation: fn(Scope) -> Element,
-    failure_redirection_limit: fn(Scope) -> Element,
 }
 
 impl<R: Routable> Clone for GenericRouterContext<R> {
@@ -66,8 +62,6 @@ impl<R: Routable> Clone for GenericRouterContext<R> {
             subscriber_update: self.subscriber_update.clone(),
             routing_callback: self.routing_callback.clone(),
             failure_external_navigation: self.failure_external_navigation,
-            failure_named_navigation: self.failure_named_navigation,
-            failure_redirection_limit: self.failure_redirection_limit,
         }
     }
 }
@@ -88,6 +82,7 @@ where
             can_go_forward: false,
             prefix: Default::default(),
             history: cfg.history,
+            unresolved_error: None,
         }));
 
         let subscriber_update = mark_dirty.clone();
@@ -101,8 +96,6 @@ where
             routing_callback: cfg.on_update,
 
             failure_external_navigation: cfg.failure_external_navigation,
-            failure_named_navigation: cfg.failure_named_navigation,
-            failure_redirection_limit: cfg.failure_redirection_limit,
         };
 
         // set the updater
@@ -134,46 +127,61 @@ where
     ///
     /// Will fail silently if there is no previous location to go to.
     pub fn go_back(&self) {
-        self.state.write().unwrap().history.go_back();
-        self.update_subscribers();
+        {
+            self.state.write().unwrap().history.go_back();
+        }
+
+        self.change_route();
     }
 
     /// Go back to the next location.
     ///
     /// Will fail silently if there is no next location to go to.
     pub fn go_forward(&self) {
-        self.state.write().unwrap().history.go_forward();
-        self.update_subscribers();
+        {
+            self.state.write().unwrap().history.go_forward();
+        }
+
+        self.change_route();
     }
 
     /// Push a new location.
     ///
     /// The previous location will be available to go back to.
-    pub fn push(&self, target: impl Into<NavigationTarget<R>>) -> Option<NavigationFailure<R>> {
+    pub fn push(
+        &self,
+        target: impl Into<NavigationTarget<R>>,
+    ) -> Option<ExternalNavigationFailure> {
         let target = target.into();
-        let mut state = self.state_mut();
         match target {
-            NavigationTarget::Internal(p) => state.history.push(p),
+            NavigationTarget::Internal(p) => {
+                let mut state = self.state_mut();
+                state.history.push(p)
+            }
             NavigationTarget::External(e) => return self.external(e),
         }
 
-        self.update_subscribers();
-        None
+        self.change_route()
     }
 
     /// Replace the current location.
     ///
     /// The previous location will **not** be available to go back to.
-    pub fn replace(&self, target: impl Into<NavigationTarget<R>>) -> Option<NavigationFailure<R>> {
+    pub fn replace(
+        &self,
+        target: impl Into<NavigationTarget<R>>,
+    ) -> Option<ExternalNavigationFailure> {
         let target = target.into();
-        let mut state = self.state_mut();
-        match target {
-            NavigationTarget::Internal(p) => state.history.replace(p),
-            NavigationTarget::External(e) => return self.external(e),
+
+        {
+            let mut state = self.state_mut();
+            match target {
+                NavigationTarget::Internal(p) => state.history.replace(p),
+                NavigationTarget::External(e) => return self.external(e),
+            }
         }
 
-        self.update_subscribers();
-        None
+        self.change_route()
     }
 
     /// The route that is currently active.
@@ -189,11 +197,18 @@ where
         self.state.read().unwrap().prefix.clone()
     }
 
-    fn external(&self, external: String) -> Option<NavigationFailure<R>> {
+    fn external(&self, external: String) -> Option<ExternalNavigationFailure> {
         let mut state = self.state_mut();
         match state.history.external(external.clone()) {
             true => None,
-            false => Some(NavigationFailure::External(external)),
+            false => {
+                let failure = ExternalNavigationFailure(external);
+                state.unresolved_error = Some(failure.clone());
+
+                self.update_subscribers();
+
+                Some(failure)
+            }
         }
     }
 
@@ -216,614 +231,37 @@ where
             (self.subscriber_update)(id);
         }
     }
-}
 
-// #[cfg(test)]
-// mod tests {
-//     //! The tests for [`RouterContext`] test various functions that are not exposed as public.
-//     //! However, several of those have an observable effect on the behavior of exposed functions.
-//     //!
-//     //! The alternative would be to send messages via the services channel and calling one of the
-//     //! `run` functions. However, for readability and clarity, it was chosen to directly call the
-//     //! private functions.
-
-//     use std::sync::Mutex;
-
-//     use crate::{
-//         history::MemoryHistory,
-//         routes::{ParameterRoute, Route, RouteContent},
-//     };
-
-//     use super::*;
-
-//     fn test_segment() -> Segment<&'static str> {
-//         Segment::content(RouteContent::Content(ContentAtom("index")))
-//             .fixed(
-//                 "fixed",
-//                 Route::content(RouteContent::Content(ContentAtom("fixed"))).name::<bool>(),
-//             )
-//             .fixed(
-//                 "redirect",
-//                 Route::content(RouteContent::Redirect(NavigationTarget::Internal(
-//                     String::from("fixed"),
-//                 ))),
-//             )
-//             .fixed(
-//                 "redirection-loop",
-//                 Route::content(RouteContent::Redirect(NavigationTarget::Internal(
-//                     String::from("/redirection-loop"),
-//                 ))),
-//             )
-//             .fixed(
-//                 "%F0%9F%8E%BA",
-//                 Route::content(RouteContent::Content(ContentAtom("🎺"))),
-//             )
-//             .catch_all(ParameterRoute::empty::<bool>())
-//     }
-
-//     #[test]
-//     fn new_provides_update_to_history() {
-//         struct TestHistory {}
-
-//         impl HistoryProvider for TestHistory {
-//             fn current_route(&self) -> String {
-//                 todo!()
-//             }
-
-//             fn current_query(&self) -> Option<String> {
-//                 todo!()
-//             }
-
-//             fn go_back(&mut self) {
-//                 todo!()
-//             }
-
-//             fn go_forward(&mut self) {
-//                 todo!()
-//             }
-
-//             fn push(&mut self, _path: String) {
-//                 todo!()
-//             }
-
-//             fn replace(&mut self, _path: String) {
-//                 todo!()
-//             }
-
-//             fn updater(&mut self, callback: Arc<dyn Fn() + Send + Sync>) {
-//                 callback();
-//             }
-//         }
-
-//         let (mut s, _, _) = RouterContext::<_, u8>::new(
-//             test_segment(),
-//             Box::new(TestHistory {}),
-//             Arc::new(|_| {}),
-//             None,
-//             ContentAtom("external target"),
-//             ContentAtom("named target"),
-//             ContentAtom("redirect limit"),
-//         );
-
-//         assert!(matches!(
-//             s.receiver.try_next().unwrap().unwrap(),
-//             RouterMessage::Update
-//         ));
-//     }
-
-//     #[test]
-//     fn update_routing() {
-//         let (mut s, _, _) = RouterContext::<_, u8>::new(
-//             test_segment(),
-//             Box::new(MemoryHistory::with_initial_path("/fixed?test=value").unwrap()),
-//             Arc::new(|_| {}),
-//             None,
-//             ContentAtom("external target"),
-//             ContentAtom("named target"),
-//             ContentAtom("redirect limit"),
-//         );
-//         assert_eq!(s.names, s.state.try_read().unwrap().name_map);
-//         s.init();
-
-//         let state = s.state.try_read().unwrap();
-//         assert_eq!(state.content, vec![ContentAtom("fixed")]);
-//         assert_eq!(state.names, {
-//             let mut r = HashSet::new();
-//             r.insert(Name::of::<bool>());
-//             r
-//         });
-//         assert!(state.parameters.is_empty());
-//         assert_eq!(state.path, String::from("/fixed"));
-//         assert_eq!(state.query, Some(String::from("test=value")));
-//         assert!(!state.can_go_back);
-//         assert!(!state.can_go_forward);
-//         assert_eq!(s.names, state.name_map);
-//     }
-
-//     #[test]
-//     fn update_routing_root_index() {
-//         let (mut s, _, _) = RouterContext::<_, u8>::new(
-//             test_segment(),
-//             Box::<MemoryHistory>::default(),
-//             Arc::new(|_| {}),
-//             None,
-//             ContentAtom("external target"),
-//             ContentAtom("named target"),
-//             ContentAtom("redirect limit"),
-//         );
-//         s.init();
-
-//         let state = s.state.try_read().unwrap();
-//         assert_eq!(state.content, vec![ContentAtom("index")]);
-//         assert_eq!(state.names, {
-//             let mut r = HashSet::new();
-//             r.insert(Name::of::<RootIndex>());
-//             r
-//         });
-//         assert!(state.parameters.is_empty());
-//         assert_eq!(state.path, String::from("/"));
-//         assert!(state.query.is_none());
-//         assert!(state.prefix.is_none());
-//         assert!(!state.can_go_back);
-//         assert!(!state.can_go_forward);
-//     }
-
-//     #[test]
-//     fn update_routing_redirect() {
-//         let (mut s, _, _) = RouterContext::<_, u8>::new(
-//             test_segment(),
-//             Box::new(MemoryHistory::with_initial_path("/redirect").unwrap()),
-//             Arc::new(|_| {}),
-//             None,
-//             ContentAtom("external target"),
-//             ContentAtom("named target"),
-//             ContentAtom("redirect limit"),
-//         );
-//         s.init();
-
-//         let state = s.state.try_read().unwrap();
-//         assert_eq!(state.content, vec![ContentAtom("fixed")]);
-//         assert_eq!(state.names, {
-//             let mut r = HashSet::new();
-//             r.insert(Name::of::<bool>());
-//             r
-//         });
-//         assert!(state.parameters.is_empty());
-//         assert_eq!(state.path, String::from("/fixed"));
-//         assert!(!state.can_go_back);
-//         assert!(!state.can_go_forward);
-//     }
-
-//     #[test]
-//     #[should_panic = "reached redirect limit of 25"]
-//     #[cfg(debug_assertions)]
-//     fn update_routing_redirect_debug() {
-//         let (mut s, _, _) = RouterContext::<_, u8>::new(
-//             test_segment(),
-//             Box::new(MemoryHistory::with_initial_path("/redirection-loop").unwrap()),
-//             Arc::new(|_| {}),
-//             None,
-//             ContentAtom("external target"),
-//             ContentAtom("named target"),
-//             ContentAtom("redirect limit"),
-//         );
-//         s.init();
-//     }
-
-//     #[test]
-//     #[cfg(not(debug_assertions))]
-//     fn update_routing_redirect_release() {
-//         let (mut s, _, _) = RouterContext::<_, u8>::new(
-//             test_segment(),
-//             Box::new(MemoryHistory::with_initial_path("/redirection-loop").unwrap()),
-//             Arc::new(|_| {}),
-//             None,
-//             ContentAtom("external target"),
-//             ContentAtom("named target"),
-//             ContentAtom("redirect limit"),
-//         );
-//         s.init();
-
-//         let state = s.state.try_read().unwrap();
-//         assert_eq!(state.content, vec![ContentAtom("redirect limit")]);
-//         assert_eq!(state.names, {
-//             let mut r = HashSet::new();
-//             r.insert(Name::of::<FailureRedirectionLimit>());
-//             r
-//         });
-//         assert!(state.parameters.is_empty());
-//         assert_eq!(state.path, String::from("/redirection-loop"));
-//         assert_eq!(state.can_go_back, false);
-//         assert_eq!(state.can_go_forward, false);
-//     }
-
-//     #[test]
-//     fn update_subscribers() {
-//         let ids = Arc::new(Mutex::new(Vec::new()));
-//         let ids2 = Arc::clone(&ids);
-
-//         let (mut s, _, _) = RouterContext::<_, u8>::new(
-//             Segment::empty(),
-//             Box::<MemoryHistory>::default(),
-//             Arc::new(move |id| {
-//                 ids2.lock().unwrap().push(id);
-//             }),
-//             None,
-//             ContentAtom("external target"),
-//             ContentAtom("named target"),
-//             ContentAtom("redirect limit"),
-//         );
-
-//         let id0 = Arc::new(0);
-//         s.subscribe(Arc::clone(&id0));
-
-//         let id1 = Arc::new(1);
-//         s.subscribe(Arc::clone(&id1));
-
-//         let id1 = Arc::try_unwrap(id1).unwrap();
-//         s.update_subscribers();
-
-//         assert_eq!(s.subscribers.len(), 1);
-//         assert_eq!(s.subscribers[0].upgrade().unwrap(), id0);
-//         assert_eq!(*ids.lock().unwrap(), vec![*id0, id1, *id0]);
-//     }
-
-//     #[test]
-//     fn push_internal() {
-//         let (mut s, _, _) = RouterContext::<_, u8>::new(
-//             test_segment(),
-//             Box::<MemoryHistory>::default(),
-//             Arc::new(|_| {}),
-//             None,
-//             ContentAtom("external target"),
-//             ContentAtom("named target"),
-//             ContentAtom("redirect limit"),
-//         );
-//         s.push(NavigationTarget::Internal(String::from("/fixed")));
-//         s.init();
-
-//         let state = s.state.try_read().unwrap();
-//         assert_eq!(state.content, vec![ContentAtom("fixed")]);
-//         assert_eq!(state.names, {
-//             let mut r = HashSet::new();
-//             r.insert(Name::of::<bool>());
-//             r
-//         });
-//         assert!(state.parameters.is_empty());
-//         assert_eq!(state.path, String::from("/fixed"));
-//         assert!(state.can_go_back);
-//         assert!(!state.can_go_forward);
-//     }
-
-//     #[test]
-//     fn push_named() {
-//         let (mut s, _, _) = RouterContext::<_, u8>::new(
-//             test_segment(),
-//             Box::<MemoryHistory>::default(),
-//             Arc::new(|_| {}),
-//             None,
-//             ContentAtom("external target"),
-//             ContentAtom("named target"),
-//             ContentAtom("redirect limit"),
-//         );
-//         s.push(NavigationTarget::named::<bool>());
-//         s.init();
-
-//         let state = s.state.try_read().unwrap();
-//         assert_eq!(state.content, vec![ContentAtom("fixed")]);
-//         assert_eq!(state.names, {
-//             let mut r = HashSet::new();
-//             r.insert(Name::of::<bool>());
-//             r
-//         });
-//         assert!(state.parameters.is_empty());
-//         assert_eq!(state.path, String::from("/fixed"));
-//         assert!(state.can_go_back);
-//         assert!(!state.can_go_forward);
-//     }
-
-//     #[test]
-//     fn push_external() {
-//         let (mut s, tx, _) = RouterContext::<_, u8>::new(
-//             test_segment(),
-//             Box::new(MemoryHistory::with_initial_path("/fixed").unwrap()),
-//             Arc::new(|_| {}),
-//             None,
-//             ContentAtom("external target"),
-//             ContentAtom("named target"),
-//             ContentAtom("redirect limit"),
-//         );
-//         s.init();
-//         tx.unbounded_send(RouterMessage::Push(NavigationTarget::External(
-//             String::from("https://dioxuslabs.com/"),
-//         )))
-//         .unwrap();
-//         s.run_current();
-
-//         let state = s.state.try_read().unwrap();
-//         assert_eq!(state.content, vec![ContentAtom("external target")]);
-//         assert_eq!(state.names, {
-//             let mut r = HashSet::new();
-//             r.insert(Name::of::<FailureExternalNavigation>());
-//             r
-//         });
-//         assert_eq!(state.parameters, {
-//             let mut r = HashMap::new();
-//             r.insert(
-//                 Name::of::<FailureExternalNavigation>(),
-//                 String::from("https://dioxuslabs.com/"),
-//             );
-//             r
-//         });
-//         assert_eq!(state.path, String::from("/fixed"));
-//         assert!(!state.can_go_back);
-//         assert!(!state.can_go_forward);
-//     }
-
-//     #[test]
-//     fn replace_named() {
-//         let (mut s, _, _) = RouterContext::<_, u8>::new(
-//             test_segment(),
-//             Box::<MemoryHistory>::default(),
-//             Arc::new(|_| {}),
-//             None,
-//             ContentAtom("external target"),
-//             ContentAtom("named target"),
-//             ContentAtom("redirect limit"),
-//         );
-//         s.replace(NavigationTarget::named::<bool>());
-//         s.init();
-
-//         let state = s.state.try_read().unwrap();
-//         assert_eq!(state.content, vec![ContentAtom("fixed")]);
-//         assert_eq!(state.names, {
-//             let mut r = HashSet::new();
-//             r.insert(Name::of::<bool>());
-//             r
-//         });
-//         assert!(state.parameters.is_empty());
-//         assert_eq!(state.path, String::from("/fixed"));
-//         assert!(!state.can_go_back);
-//         assert!(!state.can_go_forward);
-//     }
-
-//     #[test]
-//     fn replace_internal() {
-//         let (mut s, _, _) = RouterContext::<_, u8>::new(
-//             test_segment(),
-//             Box::<MemoryHistory>::default(),
-//             Arc::new(|_| {}),
-//             None,
-//             ContentAtom("external target"),
-//             ContentAtom("named target"),
-//             ContentAtom("redirect limit"),
-//         );
-//         s.replace(NavigationTarget::Internal(String::from("/fixed")));
-//         s.init();
-
-//         let state = s.state.try_read().unwrap();
-//         assert_eq!(state.content, vec![ContentAtom("fixed")]);
-//         assert_eq!(state.names, {
-//             let mut r = HashSet::new();
-//             r.insert(Name::of::<bool>());
-//             r
-//         });
-//         assert!(state.parameters.is_empty());
-//         assert_eq!(state.path, String::from("/fixed"));
-//         assert!(!state.can_go_back);
-//         assert!(!state.can_go_forward);
-//     }
-
-//     #[test]
-//     fn replace_external() {
-//         let (mut s, tx, _) = RouterContext::<_, u8>::new(
-//             test_segment(),
-//             Box::new(MemoryHistory::with_initial_path("/fixed").unwrap()),
-//             Arc::new(|_| {}),
-//             None,
-//             ContentAtom("external target"),
-//             ContentAtom("named target"),
-//             ContentAtom("redirect limit"),
-//         );
-//         s.init();
-//         tx.unbounded_send(RouterMessage::Replace(NavigationTarget::External(
-//             String::from("https://dioxuslabs.com/"),
-//         )))
-//         .unwrap();
-//         s.run_current();
-
-//         let state = s.state.try_read().unwrap();
-//         assert_eq!(state.content, vec![ContentAtom("external target")]);
-//         assert_eq!(state.names, {
-//             let mut r = HashSet::new();
-//             r.insert(Name::of::<FailureExternalNavigation>());
-//             r
-//         });
-//         assert_eq!(state.parameters, {
-//             let mut r = HashMap::new();
-//             r.insert(
-//                 Name::of::<FailureExternalNavigation>(),
-//                 String::from("https://dioxuslabs.com/"),
-//             );
-//             r
-//         });
-//         assert_eq!(state.path, String::from("/fixed"));
-//         assert!(!state.can_go_back);
-//         assert!(!state.can_go_forward);
-//     }
-
-//     #[test]
-//     fn subscribe() {
-//         let (mut s, _, _) = RouterContext::<_, u8>::new(
-//             Segment::empty(),
-//             Box::<MemoryHistory>::default(),
-//             Arc::new(|_| {}),
-//             None,
-//             ContentAtom("external target"),
-//             ContentAtom("named target"),
-//             ContentAtom("redirect limit"),
-//         );
-
-//         let id = Arc::new(0);
-//         s.subscribe(Arc::clone(&id));
-
-//         assert_eq!(s.subscribers.len(), 1);
-//         assert_eq!(s.subscribers[0].upgrade().unwrap(), id);
-//     }
-
-//     #[test]
-//     fn routing_callback() {
-//         let paths = Arc::new(Mutex::new(Vec::new()));
-//         let paths2 = Arc::clone(&paths);
-
-//         let (mut s, c, _) = RouterContext::<_, u8>::new(
-//             test_segment(),
-//             Box::new(MemoryHistory::with_initial_path("/fixed").unwrap()),
-//             Arc::new(|_| {}),
-//             Some(Arc::new(move |state| {
-//                 paths2.lock().unwrap().push(state.path.clone());
-//                 Some("/%F0%9F%8E%BA".into())
-//             })),
-//             ContentAtom("external target"),
-//             ContentAtom("named target"),
-//             ContentAtom("redirect limit"),
-//         );
-
-//         assert!(paths.lock().unwrap().is_empty());
-
-//         s.init();
-//         assert_eq!(*paths.lock().unwrap(), vec![String::from("/fixed")]);
-
-//         c.unbounded_send(RouterMessage::Update).unwrap();
-//         s.run_current();
-//         assert_eq!(
-//             *paths.lock().unwrap(),
-//             vec![String::from("/fixed"), String::from("/%F0%9F%8E%BA")]
-//         );
-
-//         let state = s.state.try_read().unwrap();
-//         assert_eq!(state.content, vec![ContentAtom("🎺")])
-//     }
-
-//     #[test]
-//     fn url_decoding_do() {
-//         let (mut s, _, _) = RouterContext::<_, u8>::new(
-//             test_segment(),
-//             Box::new(MemoryHistory::with_initial_path("/%F0%9F%A5%B3").unwrap()),
-//             Arc::new(|_| {}),
-//             None,
-//             ContentAtom("external target"),
-//             ContentAtom("named target"),
-//             ContentAtom("redirect limit"),
-//         );
-//         s.init();
-
-//         let state = s.state.try_read().unwrap();
-//         assert!(state.content.is_empty());
-//         assert!(state.names.is_empty());
-//         assert_eq!(state.parameters, {
-//             let mut r = HashMap::new();
-//             r.insert(Name::of::<bool>(), String::from("🥳"));
-//             r
-//         });
-//         assert_eq!(state.path, String::from("/%F0%9F%A5%B3"));
-//         assert!(!state.can_go_back);
-//         assert!(!state.can_go_forward);
-//     }
-
-//     #[test]
-//     fn url_decoding_do_not() {
-//         let (mut s, c, _) = RouterContext::<_, u8>::new(
-//             test_segment(),
-//             Box::new(MemoryHistory::with_initial_path("/%F0%9F%8E%BA").unwrap()),
-//             Arc::new(|_| {}),
-//             None,
-//             ContentAtom("external target"),
-//             ContentAtom("named target"),
-//             ContentAtom("redirect limit"),
-//         );
-//         s.init();
-//         c.unbounded_send(RouterMessage::Update).unwrap();
-//         s.run_current();
-
-//         let state = s.state.try_read().unwrap();
-//         assert_eq!(state.content, vec![ContentAtom("🎺")]);
-//         assert!(state.names.is_empty());
-//         assert!(state.parameters.is_empty());
-//         assert_eq!(state.path, String::from("/%F0%9F%8E%BA"));
-//         assert!(!state.can_go_back);
-//         assert!(!state.can_go_forward);
-//     }
-
-//     #[test]
-//     fn prefix() {
-//         struct TestHistory {}
-
-//         impl HistoryProvider for TestHistory {
-//             fn current_route(&self) -> String {
-//                 String::from("/")
-//             }
-
-//             fn current_query(&self) -> Option<String> {
-//                 None
-//             }
-
-//             fn current_prefix(&self) -> Option<String> {
-//                 Some(String::from("/prefix"))
-//             }
-
-//             fn can_go_back(&self) -> bool {
-//                 false
-//             }
-
-//             fn can_go_forward(&self) -> bool {
-//                 false
-//             }
-
-//             fn go_back(&mut self) {
-//                 todo!()
-//             }
-
-//             fn go_forward(&mut self) {
-//                 todo!()
-//             }
-
-//             fn push(&mut self, _path: String) {
-//                 todo!()
-//             }
-
-//             fn replace(&mut self, _path: String) {
-//                 todo!()
-//             }
-
-//             fn updater(&mut self, callback: Arc<dyn Fn() + Send + Sync>) {
-//                 callback();
-//             }
-//         }
-
-//         let (mut s, _, _) = RouterContext::<_, u8>::new(
-//             test_segment(),
-//             Box::new(TestHistory {}),
-//             Arc::new(|_| {}),
-//             None,
-//             ContentAtom("external target"),
-//             ContentAtom("named target"),
-//             ContentAtom("redirect limit"),
-//         );
-//         s.init();
-
-//         let state = s.state.try_read().unwrap();
-//         assert_eq!(state.content, vec![ContentAtom("index")]);
-//         assert_eq!(state.names, {
-//             let mut r = HashSet::new();
-//             r.insert(Name::of::<RootIndex>());
-//             r
-//         });
-//         assert!(state.parameters.is_empty());
-//         assert_eq!(state.path, String::from("/"));
-//         assert!(state.query.is_none());
-//         assert_eq!(state.prefix, Some(String::from("/prefix")));
-//         assert!(!state.can_go_back);
-//         assert!(!state.can_go_forward);
-//     }
-// }
+    /// Clear any unresolved errors
+    pub fn clear_error(&self) {
+        self.state.write().unwrap().unresolved_error = None;
+
+        self.update_subscribers();
+    }
+
+    pub(crate) fn render_error<'a>(&self, cx: Scope<'a>) -> Element<'a> {
+        self.state
+            .read()
+            .unwrap()
+            .unresolved_error
+            .as_ref()
+            .and_then(|_| (self.failure_external_navigation)(cx))
+    }
+
+    fn change_route(&self) -> Option<ExternalNavigationFailure> {
+        if let Some(callback) = &self.routing_callback {
+            let myself = self.clone();
+            if let Some(new) = callback(myself) {
+                let mut state = self.state_mut();
+                match new {
+                    NavigationTarget::Internal(p) => state.history.replace(p),
+                    NavigationTarget::External(e) => return self.external(e),
+                }
+            }
+        }
+
+        self.update_subscribers();
+
+        None
+    }
+}

+ 0 - 1
packages/router/src/history/memory.rs

@@ -18,7 +18,6 @@ where
     /// Create a [`MemoryHistory`] starting at `path`.
     ///
     /// ```rust
-    /// # use dioxus_router::history::{HistoryProvider, MemoryHistory};
     /// # use dioxus_router::prelude::*;
     /// # use serde::{Deserialize, Serialize};
     /// # use dioxus::prelude::*;

+ 7 - 10
packages/router/src/history/mod.rs

@@ -1,10 +1,14 @@
 //! History Integration
 //!
-//! dioxus-router-core relies on so-called [`HistoryProvider`]s to store the current URL, and possibly a
+//! dioxus-router-core relies on [`HistoryProvider`]s to store the current Route, and possibly a
 //! history (i.e. a browsers back button) and future (i.e. a browsers forward button).
 //!
-//! To integrate dioxus-router-core with a any type of history, all you have to do is implement the
-//! [`HistoryProvider`] trait. dioxus-router-core also comes with some (for now one) default implementations.
+//! To integrate dioxus-router with a any type of history, all you have to do is implement the
+//! [`HistoryProvider`] trait.
+//!
+//! dioxus-router contains two built in history providers:
+//! 1) [`MemoryHistory`] for desktop/mobile/ssr platforms
+//! 2) [`WebHistory`] for web platforms
 
 use std::sync::Arc;
 
@@ -42,7 +46,6 @@ pub trait HistoryProvider<R: Routable> {
     /// **Must start** with `/`. **Must _not_ contain** the prefix.
     ///
     /// ```rust
-    /// # use dioxus_router::history::{HistoryProvider, MemoryHistory};
     /// # use dioxus_router::prelude::*;
     /// # use serde::{Deserialize, Serialize};
     /// # use dioxus::prelude::*;
@@ -82,7 +85,6 @@ pub trait HistoryProvider<R: Routable> {
     /// If a [`HistoryProvider`] cannot know this, it should return [`true`].
     ///
     /// ```rust
-    /// # use dioxus_router::history::{HistoryProvider, MemoryHistory};
     /// # use dioxus_router::prelude::*;
     /// # use serde::{Deserialize, Serialize};
     /// # use dioxus::prelude::*;   
@@ -110,7 +112,6 @@ pub trait HistoryProvider<R: Routable> {
     /// might be called, even if `can_go_back` returns [`false`].
     ///
     /// ```rust
-    /// # use dioxus_router::history::{HistoryProvider, MemoryHistory};
     /// # use dioxus_router::prelude::*;
     /// # use serde::{Deserialize, Serialize};
     /// # use dioxus::prelude::*;
@@ -144,7 +145,6 @@ pub trait HistoryProvider<R: Routable> {
     /// If a [`HistoryProvider`] cannot know this, it should return [`true`].
     ///
     /// ```rust
-    /// # use dioxus_router::history::{HistoryProvider, MemoryHistory};
     /// # use dioxus_router::prelude::*;
     /// # use serde::{Deserialize, Serialize};
     /// # use dioxus::prelude::*;
@@ -179,7 +179,6 @@ pub trait HistoryProvider<R: Routable> {
     /// might be called, even if `can_go_forward` returns [`false`].
     ///
     /// ```rust
-    /// # use dioxus_router::history::{HistoryProvider, MemoryHistory};
     /// # use dioxus_router::prelude::*;
     /// # use serde::{Deserialize, Serialize};
     /// # use dioxus::prelude::*;
@@ -214,7 +213,6 @@ pub trait HistoryProvider<R: Routable> {
     /// 3. Clear the navigation future.
     ///
     /// ```rust
-    /// # use dioxus_router::history::{HistoryProvider, MemoryHistory};
     /// # use dioxus_router::prelude::*;
     /// # use serde::{Deserialize, Serialize};
     /// # use dioxus::prelude::*;
@@ -245,7 +243,6 @@ pub trait HistoryProvider<R: Routable> {
     /// untouched.
     ///
     /// ```rust
-    /// # use dioxus_router::history::{HistoryProvider, MemoryHistory};
     /// # use dioxus_router::prelude::*;
     /// # use serde::{Deserialize, Serialize};
     /// # use dioxus::prelude::*;

+ 2 - 3
packages/router/src/hooks/use_route.rs

@@ -10,14 +10,13 @@ use crate::utils::use_router_internal::use_router_internal;
 /// - Otherwise the current route.
 ///
 /// # Panic
-/// - When the calling component is not nested within another component calling the [`use_router`]
-///   hook, but only in debug builds.
+/// - When the calling component is not nested within a [`GenericRouter`] component durring a debug build.
 ///
 /// # Example
 /// ```rust
 /// # use dioxus::prelude::*;
 /// # use serde::{Deserialize, Serialize};
-/// # use dioxus_router::{history::*, prelude::*};
+/// # use dioxus_router::{prelude::*};
 ///
 /// #[derive(Clone, Serialize, Deserialize, Routable)]
 /// enum Route {

+ 1 - 1
packages/router/src/hooks/use_router.rs

@@ -9,7 +9,7 @@ use crate::{
 ///
 /// ```rust
 /// # use dioxus::prelude::*;
-/// # use dioxus_router::{history::*, prelude::*};
+/// # use dioxus_router::prelude::*;
 /// # use serde::{Deserialize, Serialize};
 /// #[derive(Clone, Serialize, Deserialize, Routable)]
 /// enum Route {

+ 6 - 4
packages/router/src/lib.rs

@@ -7,8 +7,9 @@ pub mod navigation;
 pub mod routable;
 
 /// Components interacting with the router.
-pub mod components {
-    pub(crate) mod default_errors;
+mod components {
+    mod default_errors;
+    pub use default_errors::*;
 
     mod history_buttons;
     pub use history_buttons::*;
@@ -31,10 +32,10 @@ mod contexts {
 
 mod router_cfg;
 
-pub mod history;
+mod history;
 
 /// Hooks for interacting with the router in components.
-pub mod hooks {
+mod hooks {
     mod use_router;
     pub use use_router::*;
 
@@ -48,6 +49,7 @@ pub mod prelude {
     pub use crate::contexts::*;
     pub use crate::history::*;
     pub use crate::hooks::*;
+    pub use crate::navigation::*;
     pub use crate::routable::*;
     pub use crate::router_cfg::RouterConfiguration;
     pub use dioxus_router_macro::Routable;

+ 24 - 17
packages/router/src/navigation.rs

@@ -1,13 +1,17 @@
 //! Types pertaining to navigation.
 
-use std::{fmt::Display, str::FromStr};
+use std::{
+    convert::TryFrom,
+    fmt::{Debug, Display},
+    str::FromStr,
+};
 
 use url::{ParseError, Url};
 
 use crate::routable::Routable;
 
 /// A target for the router to navigate to.
-#[derive(Clone, Debug, PartialEq, Eq)]
+#[derive(Clone, PartialEq, Eq, Debug)]
 pub enum NavigationTarget<R: Routable> {
     /// An internal path that the router can navigate to by itself.
     ///
@@ -26,7 +30,7 @@ pub enum NavigationTarget<R: Routable> {
     ///     Index {},
     /// }
     /// let explicit = NavigationTarget::Internal(Route::Index {});
-    /// let implicit: NavigationTarget::<Route> = "/".into();
+    /// let implicit: NavigationTarget::<Route> = "/".parse().unwrap();
     /// assert_eq!(explicit, implicit);
     /// ```
     Internal(R),
@@ -47,25 +51,17 @@ pub enum NavigationTarget<R: Routable> {
     ///     Index {},
     /// }
     /// let explicit = NavigationTarget::<Route>::External(String::from("https://dioxuslabs.com/"));
-    /// let implicit: NavigationTarget::<Route> = "https://dioxuslabs.com/".into();
+    /// let implicit: NavigationTarget::<Route> = "https://dioxuslabs.com/".parse().unwrap();
     /// assert_eq!(explicit, implicit);
     /// ```
     External(String),
 }
 
-impl<R: Routable> From<&str> for NavigationTarget<R>
-where
-    <R as FromStr>::Err: Display,
-{
-    fn from(value: &str) -> Self {
-        Self::from_str(value).unwrap_or_else(|err| match err {
-            NavigationTargetParseError::InvalidUrl(e) => {
-                panic!("Failed to parse `{}` as a URL: {}", value, e)
-            }
-            NavigationTargetParseError::InvalidInternalURL(e) => {
-                panic!("Failed to parse `{}` as a `Routable`: {}", value, e)
-            }
-        })
+impl<R: Routable> TryFrom<&str> for NavigationTarget<R> {
+    type Error = NavigationTargetParseError<R>;
+
+    fn try_from(value: &str) -> Result<Self, Self::Error> {
+        value.parse()
     }
 }
 
@@ -92,6 +88,17 @@ pub enum NavigationTargetParseError<R: Routable> {
     InvalidInternalURL(<R as FromStr>::Err),
 }
 
+impl<R: Routable> Debug for NavigationTargetParseError<R> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            NavigationTargetParseError::InvalidUrl(e) => write!(f, "Invalid URL: {}", e),
+            NavigationTargetParseError::InvalidInternalURL(_) => {
+                write!(f, "Invalid internal URL")
+            }
+        }
+    }
+}
+
 impl<R: Routable> FromStr for NavigationTarget<R> {
     type Err = NavigationTargetParseError<R>;
 

+ 3 - 22
packages/router/src/router_cfg.rs

@@ -1,13 +1,10 @@
 use crate::contexts::router::RoutingCallback;
 use crate::history::HistoryProvider;
-use crate::prelude::*;
 use crate::routable::Routable;
 use dioxus::prelude::*;
 use serde::{de::DeserializeOwned, Serialize};
 
-use crate::prelude::default_errors::{
-    FailureExternalNavigation, FailureNamedNavigation, FailureRedirectionLimit,
-};
+use crate::prelude::*;
 
 /// Global configuration options for the router.
 ///
@@ -33,22 +30,8 @@ use crate::prelude::default_errors::{
 pub struct RouterConfiguration<R: Routable> {
     /// A component to render when an external navigation fails.
     ///
-    /// Defaults to a router-internal component called `FailureExternalNavigation`. It is not part
-    /// of the public API. Do not confuse it with
-    /// [`dioxus_router::prelude::FailureExternalNavigation`].
+    /// Defaults to a router-internal component called [`FailureExternalNavigation`]
     pub failure_external_navigation: fn(Scope) -> Element,
-    /// A component to render when a named navigation fails.
-    ///
-    /// Defaults to a router-internal component called `FailureNamedNavigation`. It is not part of
-    /// the public API. Do not confuse it with
-    /// [`dioxus_router::prelude::FailureNamedNavigation`].
-    pub failure_named_navigation: fn(Scope) -> Element,
-    /// A component to render when the redirect limit is reached.
-    ///
-    /// Defaults to a router-internal component called `FailureRedirectionLimit`. It is not part of
-    /// the public API. Do not confuse it with
-    /// [`dioxus_router::prelude::FailureRedirectionLimit`].
-    pub failure_redirection_limit: fn(Scope) -> Element,
     /// The [`HistoryProvider`] the router should use.
     ///
     /// Defaults to a default [`MemoryHistory`].
@@ -58,7 +41,7 @@ pub struct RouterConfiguration<R: Routable> {
     /// The callback is invoked after the routing is updated, but before components and hooks are
     /// updated.
     ///
-    /// If the callback returns a [`dioxus_router::navigation::NavigationTarget`] the router will replace the current location
+    /// If the callback returns a [`NavigationTarget`] the router will replace the current location
     /// with it. If no navigation failure was triggered, the router will then updated dependent
     /// components and hooks.
     ///
@@ -76,8 +59,6 @@ where
     fn default() -> Self {
         Self {
             failure_external_navigation: FailureExternalNavigation::<R>,
-            failure_named_navigation: FailureNamedNavigation::<R>,
-            failure_redirection_limit: FailureRedirectionLimit::<R>,
             history: Box::<MemoryHistory<R>>::default(),
             on_update: None,
         }

+ 3 - 3
packages/router/tests/via_ssr/link.rs

@@ -101,7 +101,7 @@ fn href_external() {
     fn Root(cx: Scope) -> Element {
         render! {
             Link {
-                target: "https://dioxuslabs.com/",
+                target: NavigationTarget::External("https://dioxuslabs.com/".into()),
                 "Link"
             }
         }
@@ -171,7 +171,7 @@ fn with_active_class_active() {
     fn Root(cx: Scope) -> Element {
         render! {
             Link {
-                target: "/",
+                target: Route::Root {},
                 active_class: "active_class",
                 class: "test_class",
                 "Link"
@@ -322,7 +322,7 @@ fn with_new_tab_external() {
     fn Root(cx: Scope) -> Element {
         render! {
             Link {
-                target: "https://dioxuslabs.com/",
+                target: NavigationTarget::External("https://dioxuslabs.com/".into()),
                 new_tab: true,
                 "Link"
             }

+ 1 - 1
packages/router/tests/via_ssr/outlet.rs

@@ -1,7 +1,7 @@
 #![allow(non_snake_case, unused)]
 
 use dioxus::prelude::*;
-use dioxus_router::{history::MemoryHistory, prelude::*};
+use dioxus_router::prelude::*;
 use serde::{Deserialize, Serialize};
 
 fn prepare(path: impl Into<String>) -> VirtualDom {