Bläddra i källkod

Intigrate macro with router hooks and components

Evan Almloff 2 år sedan
förälder
incheckning
e4b8fbfafe
37 ändrade filer med 1442 tillägg och 3009 borttagningar
  1. 0 1
      Cargo.toml
  2. 0 29
      packages/router-core/Cargo.toml
  3. 0 1
      packages/router-core/README.md
  4. 0 256
      packages/router-core/src/history/memory.rs
  5. 0 49
      packages/router-core/src/lib.rs
  6. 0 448
      packages/router-core/src/navigation.rs
  7. 0 97
      packages/router-core/src/navigator.rs
  8. 0 1081
      packages/router-core/src/service.rs
  9. 0 288
      packages/router-core/src/state.rs
  10. 0 245
      packages/router-core/tests/macro.rs
  11. 3 3
      packages/router-macro/src/lib.rs
  12. 1 1
      packages/router-macro/src/nest.rs
  13. 1 1
      packages/router-macro/src/query.rs
  14. 2 2
      packages/router-macro/src/route.rs
  15. 2 2
      packages/router-macro/src/segment.rs
  16. 4 7
      packages/router/Cargo.toml
  17. 9 14
      packages/router/src/components/default_errors.rs
  18. 9 22
      packages/router/src/components/history_buttons.rs
  19. 20 26
      packages/router/src/components/link.rs
  20. 49 0
      packages/router/src/components/outlet.rs
  21. 60 0
      packages/router/src/components/router.rs
  22. 29 0
      packages/router/src/contexts/outlet.rs
  23. 806 9
      packages/router/src/contexts/router.rs
  24. 230 0
      packages/router/src/history/memory.rs
  25. 10 17
      packages/router/src/history/mod.rs
  26. 2 2
      packages/router/src/history/web.rs
  27. 2 2
      packages/router/src/history/web_hash.rs
  28. 0 0
      packages/router/src/history/web_scroll.rs
  29. 0 71
      packages/router/src/hooks/use_navigate.rs
  30. 4 10
      packages/router/src/hooks/use_route.rs
  31. 7 195
      packages/router/src/hooks/use_router.rs
  32. 14 14
      packages/router/src/lib.rs
  33. 69 0
      packages/router/src/navigation.rs
  34. 18 102
      packages/router/src/routable.rs
  35. 71 0
      packages/router/src/router_cfg.rs
  36. 0 0
      packages/router/src/utils/sitemap.rs
  37. 20 14
      packages/router/src/utils/use_router_internal.rs

+ 0 - 1
Cargo.toml

@@ -3,7 +3,6 @@ members = [
     "packages/dioxus",
     "packages/core",
     "packages/core-macro",
-    "packages/router-core",
     "packages/router-macro",
     "packages/router",
     "packages/html",

+ 0 - 29
packages/router-core/Cargo.toml

@@ -1,29 +0,0 @@
-[package]
-name = "dioxus-router-core"
-version = "0.1.0"
-edition = "2021"
-
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
-
-[dependencies]
-async-lock = "2.7.0"
-either = "1.8.0"
-futures-channel = "0.3.25"
-futures-util = "0.3.25"
-gloo = { version = "0.8.0", optional = true }
-log = "0.4.17"
-regex = { version = "1.6.0", optional = true }
-serde = { version = "1.0.147", optional = true, features = ["derive"] }
-serde-wasm-bindgen = { version = "0.4.5", optional = true }
-serde_urlencoded = { version = "0.7.1", optional = true }
-url = "2.3.1"
-urlencoding = "2.1.2"
-wasm-bindgen = { version = "0.2.83", optional = true }
-web-sys = { version = "0.3.60", optional = true, features = ["ScrollRestoration"]}
-dioxus = { path = "../dioxus" }
-dioxus-router-macro = { path = "../router-macro" }
-
-[features]
-regex = ["dep:regex"]
-serde = ["dep:serde", "serde_urlencoded"]
-web = ["gloo", "dep:serde", "serde-wasm-bindgen", "wasm-bindgen", "web-sys"]

+ 0 - 1
packages/router-core/README.md

@@ -1 +0,0 @@
-# Dioxus Router Core

+ 0 - 256
packages/router-core/src/history/memory.rs

@@ -1,256 +0,0 @@
-use std::str::FromStr;
-
-use url::{ParseError, Url};
-
-use super::HistoryProvider;
-
-const INITIAL_URL: &str = "dioxus-router-core://initial_url.invalid/";
-
-/// A [`HistoryProvider`] that stores all navigation information in memory.
-pub struct MemoryHistory {
-    current: Url,
-    history: Vec<String>,
-    future: Vec<String>,
-}
-
-impl MemoryHistory {
-    /// Create a [`MemoryHistory`] starting at `path`.
-    ///
-    /// ```rust
-    /// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
-    /// let mut history = MemoryHistory::with_initial_path("/some/path").unwrap();
-    /// assert_eq!(history.current_path(), "/some/path");
-    /// assert_eq!(history.can_go_back(), false);
-    /// ```
-    pub fn with_initial_path(path: impl Into<String>) -> Result<Self, ParseError> {
-        let mut path = path.into();
-        if path.starts_with('/') {
-            path.remove(0);
-        }
-        let url = Url::parse(&format!("{INITIAL_URL}{path}"))?;
-
-        Ok(Self {
-            current: url,
-            ..Default::default()
-        })
-    }
-}
-
-impl Default for MemoryHistory {
-    fn default() -> Self {
-        Self {
-            current: Url::from_str(INITIAL_URL).unwrap(),
-            history: Vec::new(),
-            future: Vec::new(),
-        }
-    }
-}
-
-impl HistoryProvider for MemoryHistory {
-    fn current_path(&self) -> String {
-        self.current.path().to_string()
-    }
-
-    fn current_query(&self) -> Option<String> {
-        self.current.query().map(|q| q.to_string())
-    }
-
-    fn can_go_back(&self) -> bool {
-        !self.history.is_empty()
-    }
-
-    fn go_back(&mut self) {
-        if let Some(last) = self.history.pop() {
-            self.future.push(self.current.to_string());
-            self.current = Url::parse(&last).unwrap(/* past URLs are always valid */);
-        }
-    }
-
-    fn can_go_forward(&self) -> bool {
-        !self.future.is_empty()
-    }
-
-    fn go_forward(&mut self) {
-        if let Some(next) = self.future.pop() {
-            self.history.push(self.current.to_string());
-            self.current = Url::parse(&next).unwrap(/* future URLs are always valid */);
-        }
-    }
-
-    fn push(&mut self, path: String) {
-        let wrong = path.starts_with("//");
-        debug_assert!(
-            !wrong,
-            "cannot navigate to paths starting with \"//\": {path}"
-        );
-        if wrong {
-            return;
-        }
-
-        if let Ok(new) = self.current.join(&path) {
-            self.history.push(self.current.to_string());
-            self.current = new;
-            self.future.clear();
-        }
-    }
-
-    fn replace(&mut self, path: String) {
-        let wrong = path.starts_with("//");
-        debug_assert!(
-            !wrong,
-            "cannot navigate to paths starting with \"//\": {path}"
-        );
-        if wrong {
-            return;
-        }
-
-        if let Ok(new) = self.current.join(&path) {
-            self.current = new;
-        }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn default() {
-        let mem = MemoryHistory::default();
-        assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap());
-        assert_eq!(mem.history, Vec::<String>::new());
-        assert_eq!(mem.future, Vec::<String>::new());
-    }
-
-    #[test]
-    fn with_initial_path() {
-        let mem = MemoryHistory::with_initial_path("something").unwrap();
-        assert_eq!(
-            mem.current,
-            Url::parse(&format!("{INITIAL_URL}something")).unwrap()
-        );
-        assert_eq!(mem.history, Vec::<String>::new());
-        assert_eq!(mem.future, Vec::<String>::new());
-    }
-
-    #[test]
-    fn with_initial_path_with_leading_slash() {
-        let mem = MemoryHistory::with_initial_path("/something").unwrap();
-        assert_eq!(
-            mem.current,
-            Url::parse(&format!("{INITIAL_URL}something")).unwrap()
-        );
-        assert_eq!(mem.history, Vec::<String>::new());
-        assert_eq!(mem.future, Vec::<String>::new());
-    }
-
-    #[test]
-    fn can_go_back() {
-        let mut mem = MemoryHistory::default();
-        assert!(!mem.can_go_back());
-
-        mem.push(String::from("/test"));
-        assert!(mem.can_go_back());
-    }
-
-    #[test]
-    fn go_back() {
-        let mut mem = MemoryHistory::default();
-        mem.push(String::from("/test"));
-        mem.go_back();
-
-        assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap());
-        assert!(mem.history.is_empty());
-        assert_eq!(mem.future, vec![format!("{INITIAL_URL}test")]);
-    }
-
-    #[test]
-    fn can_go_forward() {
-        let mut mem = MemoryHistory::default();
-        assert!(!mem.can_go_forward());
-
-        mem.push(String::from("/test"));
-        mem.go_back();
-
-        assert!(mem.can_go_forward());
-    }
-
-    #[test]
-    fn go_forward() {
-        let mut mem = MemoryHistory::default();
-        mem.push(String::from("/test"));
-        mem.go_back();
-        mem.go_forward();
-
-        assert_eq!(
-            mem.current,
-            Url::parse(&format!("{INITIAL_URL}test")).unwrap()
-        );
-        assert_eq!(mem.history, vec![INITIAL_URL.to_string()]);
-        assert!(mem.future.is_empty());
-    }
-
-    #[test]
-    fn push() {
-        let mut mem = MemoryHistory::default();
-        mem.push(String::from("/test"));
-
-        assert_eq!(
-            mem.current,
-            Url::parse(&format!("{INITIAL_URL}test")).unwrap()
-        );
-        assert_eq!(mem.history, vec![INITIAL_URL.to_string()]);
-        assert!(mem.future.is_empty());
-    }
-
-    #[test]
-    #[should_panic = r#"cannot navigate to paths starting with "//": //test"#]
-    #[cfg(debug_assertions)]
-    fn push_debug() {
-        let mut mem = MemoryHistory::default();
-        mem.push(String::from("//test"));
-    }
-
-    #[test]
-    #[cfg(not(debug_assertions))]
-    fn push_release() {
-        let mut mem = MemoryHistory::default();
-        mem.push(String::from("//test"));
-
-        assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap());
-        assert!(mem.history.is_empty())
-    }
-
-    #[test]
-    fn replace() {
-        let mut mem = MemoryHistory::default();
-        mem.push(String::from("/test"));
-        mem.push(String::from("/other"));
-        mem.go_back();
-        mem.replace(String::from("/replace"));
-
-        assert_eq!(
-            mem.current,
-            Url::parse(&format!("{INITIAL_URL}replace")).unwrap()
-        );
-        assert_eq!(mem.history, vec![INITIAL_URL.to_string()]);
-        assert_eq!(mem.future, vec![format!("{INITIAL_URL}other")]);
-    }
-
-    #[test]
-    #[should_panic = r#"cannot navigate to paths starting with "//": //test"#]
-    #[cfg(debug_assertions)]
-    fn replace_debug() {
-        let mut mem = MemoryHistory::default();
-        mem.replace(String::from("//test"));
-    }
-
-    #[test]
-    #[cfg(not(debug_assertions))]
-    fn replace_release() {
-        let mut mem = MemoryHistory::default();
-        mem.replace(String::from("//test"));
-
-        assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap());
-    }
-}

+ 0 - 49
packages/router-core/src/lib.rs

@@ -1,49 +0,0 @@
-#![doc = include_str!("../README.md")]
-// #![forbid(missing_docs)]
-
-pub mod history;
-
-pub mod router;
-pub use router::*;
-
-// pub mod navigation;
-
-// mod navigator;
-// pub use navigator::*;
-
-// mod service;
-// pub use service::*;
-
-// mod state;
-// pub use state::*;
-
-// mod utils {
-//     mod sitemap;
-//     pub use sitemap::*;
-// }
-
-/// A collection of useful types most applications might need.
-pub mod prelude {
-    // pub use crate::navigation::*;
-
-    /// An external navigation failure.
-    ///
-    /// These occur when the router tries to navigate to a [`NavigationTarget::External`] and the
-    /// [`HistoryProvider`](crate::history::HistoryProvider) doesn't support that.
-    pub struct FailureExternalNavigation;
-
-    /// A named navigation failure.
-    ///
-    /// These occur when the router tries to navigate to a [`NavigationTarget::Named`] and the
-    /// specified [`Name`](crate::Name) is unknown, or a parameter is missing.
-    pub struct FailureNamedNavigation;
-
-    /// A redirection limit breach.
-    ///
-    /// These occur when the router tries to navigate to any target, but encounters 25 consecutive
-    /// redirects.
-    pub struct FailureRedirectionLimit;
-
-    /// The [`Name`](crate::Name) equivalent of `/`.
-    pub struct RootIndex;
-}

+ 0 - 448
packages/router-core/src/navigation.rs

@@ -1,448 +0,0 @@
-//! Types pertaining to navigation.
-
-use std::{collections::HashMap, str::FromStr};
-
-use url::{ParseError, Url};
-
-/// A target for the router to navigate to.
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub enum NavigationTarget<R: Routable> {
-    /// An internal path that the router can navigate to by itself.
-    ///
-    /// ```rust
-    /// # use dioxus_router_core::navigation::NavigationTarget;
-    /// let explicit = NavigationTarget::Internal(String::from("/internal"));
-    /// let implicit: NavigationTarget = "/internal".into();
-    /// assert_eq!(explicit, implicit);
-    /// ```
-    Internal(R),
-    /// An external target that the router doesn't control.
-    ///
-    /// ```rust
-    /// # use dioxus_router_core::navigation::NavigationTarget;
-    /// let explicit = NavigationTarget::External(String::from("https://dioxuslabs.com/"));
-    /// let implicit: NavigationTarget = "https://dioxuslabs.com/".into();
-    /// assert_eq!(explicit, implicit);
-    /// ```
-    External(String),
-}
-
-impl NavigationTarget {
-    /// Create a new [`NavigationTarget::Named`] with `N` as the name.
-    ///
-    /// ```rust
-    /// # use dioxus_router_core::navigation::NavigationTarget;
-    /// let target = NavigationTarget::named::<bool>();
-    /// ```
-    ///
-    /// **Note:** The dioxus-router-core documentation and tests mostly use standard Rust types. This is only
-    /// for brevity. It is recommend to use types with descriptive names, and create unit structs if
-    /// needed.
-    pub fn named<N: 'static>() -> Self {
-        Self::Named {
-            name: Name::of::<N>(),
-            parameters: HashMap::new(),
-            query: None,
-        }
-    }
-
-    /// Add a parameter to a [`NavigationTarget::Named`].
-    ///
-    /// ```rust
-    /// # use dioxus_router_core::navigation::NavigationTarget;
-    /// let target = NavigationTarget::named::<bool>().parameter::<bool>("some parameter");
-    /// ```
-    ///
-    /// **Note:** The dioxus-router-core documentation and tests mostly use standard Rust types. This is only
-    /// for brevity. It is recommend to use types with descriptive names, and create unit structs if
-    /// needed.
-    ///
-    /// # Error Handling
-    /// 1. An error occurs if `self` is any other [`NavigationTarget`] variant than
-    ///    [`NavigationTarget::Named`]. In _debug mode_ this will trigger a panic. In _release mode_
-    ///    nothing will happen.
-    /// 2. Parameters need to be unique within the [`NavigationTarget`]. In _debug mode_ the
-    ///    second call with a duplicate name will panic. In _release mode_, all calls after the
-    ///    first will be ignored.
-    pub fn parameter<N: 'static>(mut self, value: impl Into<String>) -> Self {
-        let n = Name::of::<N>();
-
-        if let Self::Named {
-            name,
-            parameters,
-            query: _,
-        } = &mut self
-        {
-            debug_assert!(
-                !parameters.contains_key(&n),
-                "duplicate parameter: {name} - {n}",
-            );
-            parameters.entry(n).or_insert_with(|| value.into());
-        } else {
-            #[cfg(debug_assertions)]
-            panic!("parameter only available for named target: {n}");
-        }
-
-        self
-    }
-
-    /// Add a parameter to a [`NavigationTarget::Named`].
-    ///
-    /// ```rust
-    /// # use dioxus_router_core::navigation::NavigationTarget;
-    /// let target = NavigationTarget::named::<bool>().query("some=query");
-    /// ```
-    ///
-    /// # Error Handling
-    /// 1. An error occurs if `self` is any other [`NavigationTarget`] variant than
-    ///    [`NavigationTarget::Named`]. In _debug mode_ this will trigger a panic. In _release mode_
-    ///    nothing will happen.
-    /// 2. This function may only be called once per [`NavigationTarget`]. In _debug mode_, the
-    ///    second call will panic. In _release mode_, all calls after the first will be ignored.
-    pub fn query(mut self, query: impl Into<Query>) -> Self {
-        if let Self::Named {
-            name,
-            parameters: _,
-            query: q,
-        } = &mut self
-        {
-            debug_assert!(q.is_none(), "query cannot be changed: {name}",);
-            q.get_or_insert(query.into());
-        } else {
-            #[cfg(debug_assertions)]
-            panic!("query only available for named target",);
-        }
-
-        self
-    }
-}
-
-/// Create a new [`NavigationTarget::Named`] with `N` as the name.
-///
-/// ```rust
-/// # use dioxus_router_core::navigation::named;
-/// let target = named::<bool>();
-/// ```
-///
-/// **Note:** The dioxus-router-core documentation and tests mostly use standard Rust types. This is only
-/// for brevity. It is recommend to use types with descriptive names, and create unit structs if
-/// needed.
-///
-/// This is a shortcut for [`NavigationTarget`]s `named` function.
-pub fn named<T: 'static>() -> NavigationTarget {
-    NavigationTarget::named::<T>()
-}
-
-impl FromStr for NavigationTarget {
-    type Err = ParseError;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        match Url::parse(s) {
-            Ok(_) => Ok(Self::External(s.to_string())),
-            Err(ParseError::RelativeUrlWithoutBase) => Ok(Self::Internal(s.to_string())),
-            Err(e) => Err(e),
-        }
-    }
-}
-
-impl<T: Into<String>> From<T> for NavigationTarget {
-    fn from(v: T) -> Self {
-        let v = v.into();
-        v.clone().parse().unwrap_or(Self::Internal(v))
-    }
-}
-
-/// A representation of a query string.
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub enum Query {
-    /// A basic query string.
-    ///
-    /// ```rust
-    /// # use dioxus_router_core::navigation::Query;
-    /// let explicit = Query::Single(String::from("some=query"));
-    /// let implicit: Query = "some=query".into();
-    /// assert_eq!(explicit, implicit);
-    /// ```
-    Single(String),
-    /// A query separated into key-value-pairs.
-    ///
-    /// ```rust
-    /// # use dioxus_router_core::navigation::Query;
-    /// let explicit = Query::List(vec![(String::from("some"), String::from("query"))]);
-    /// let implicit: Query = vec![("some", "query")].into();
-    /// assert_eq!(explicit, implicit);
-    /// ```
-    #[cfg(feature = "serde")]
-    List(Vec<(String, String)>),
-}
-
-impl Query {
-    /// Create a [`Query`] from a [`Serialize`](serde::Serialize)able object.
-    #[cfg(feature = "serde")]
-    pub fn from_serde(query: impl serde::Serialize) -> Result<Self, serde_urlencoded::ser::Error> {
-        serde_urlencoded::to_string(query).map(|q| Self::Single(q))
-    }
-}
-
-impl From<String> for Query {
-    fn from(v: String) -> Self {
-        Self::Single(v)
-    }
-}
-
-impl From<&str> for Query {
-    fn from(v: &str) -> Self {
-        v.to_string().into()
-    }
-}
-
-#[cfg(feature = "serde")]
-impl<T: Into<String>> From<Vec<(T, T)>> for Query {
-    fn from(v: Vec<(T, T)>) -> Self {
-        Self::List(v.into_iter().map(|(a, b)| (a.into(), b.into())).collect())
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn named_enum() {
-        assert_eq!(
-            NavigationTarget::named::<bool>(),
-            NavigationTarget::Named {
-                name: Name::of::<bool>(),
-                parameters: HashMap::new(),
-                query: None
-            }
-        )
-    }
-
-    #[test]
-    fn named_func() {
-        assert_eq!(
-            named::<bool>(),
-            NavigationTarget::Named {
-                name: Name::of::<bool>(),
-                parameters: HashMap::new(),
-                query: None
-            }
-        )
-    }
-
-    #[test]
-    fn parameter() {
-        assert_eq!(
-            named::<bool>()
-                .parameter::<i32>("integer")
-                .parameter::<u32>("unsigned"),
-            NavigationTarget::Named {
-                name: Name::of::<bool>(),
-                parameters: {
-                    let mut r = HashMap::new();
-                    r.insert(Name::of::<i32>(), "integer".to_string());
-                    r.insert(Name::of::<u32>(), "unsigned".to_string());
-                    r
-                },
-                query: None
-            }
-        )
-    }
-
-    #[test]
-    #[should_panic = "duplicate parameter: bool - i32"]
-    #[cfg(debug_assertions)]
-    fn parameter_duplicate_debug() {
-        named::<bool>()
-            .parameter::<i32>("integer")
-            .parameter::<i32>("duplicate");
-    }
-
-    #[test]
-    #[cfg(not(debug_assertions))]
-    fn parameter_duplicate_release() {
-        assert_eq!(
-            named::<bool>()
-                .parameter::<i32>("integer")
-                .parameter::<i32>("duplicate"),
-            NavigationTarget::Named {
-                name: Name::of::<bool>(),
-                parameters: {
-                    let mut r = HashMap::new();
-                    r.insert(Name::of::<i32>(), "integer".to_string());
-                    r
-                },
-                query: None
-            }
-        );
-    }
-
-    #[test]
-    #[should_panic = "parameter only available for named target: i32"]
-    #[cfg(debug_assertions)]
-    fn parameter_internal_debug() {
-        NavigationTarget::from("/test").parameter::<i32>("integer");
-    }
-
-    #[test]
-    #[cfg(not(debug_assertions))]
-    fn parameter_internal_release() {
-        assert_eq!(
-            NavigationTarget::from("/test").parameter::<i32>("integer"),
-            NavigationTarget::from("/test")
-        );
-    }
-
-    #[test]
-    #[should_panic = "parameter only available for named target: i32"]
-    #[cfg(debug_assertions)]
-    fn parameter_external_debug() {
-        NavigationTarget::from("https://dioxuslabs.com/").parameter::<i32>("integer");
-    }
-
-    #[test]
-    #[cfg(not(debug_assertions))]
-    fn parameter_external_release() {
-        assert_eq!(
-            NavigationTarget::from("https://dioxuslabs.com/").parameter::<i32>("integer"),
-            NavigationTarget::from("https://dioxuslabs.com/")
-        );
-    }
-
-    #[test]
-    fn query() {
-        assert_eq!(
-            named::<bool>().query("test"),
-            NavigationTarget::Named {
-                name: Name::of::<bool>(),
-                parameters: HashMap::new(),
-                query: Some(Query::Single("test".to_string()))
-            }
-        )
-    }
-
-    #[test]
-    #[should_panic = "query cannot be changed: bool"]
-    #[cfg(debug_assertions)]
-    fn query_multiple_debug() {
-        named::<bool>().query("test").query("other");
-    }
-
-    #[test]
-    #[cfg(not(debug_assertions))]
-    fn query_multiple_release() {
-        assert_eq!(
-            named::<bool>().query("test").query("other"),
-            NavigationTarget::Named {
-                name: Name::of::<bool>(),
-                parameters: HashMap::new(),
-                query: Some(Query::Single("test".to_string()))
-            }
-        )
-    }
-
-    #[test]
-    #[should_panic = "query only available for named target"]
-    #[cfg(debug_assertions)]
-    fn query_internal_debug() {
-        NavigationTarget::from("/test").query("test");
-    }
-
-    #[test]
-    #[cfg(not(debug_assertions))]
-    fn query_internal_release() {
-        assert_eq!(
-            NavigationTarget::from("/test").query("test"),
-            NavigationTarget::from("/test")
-        );
-    }
-
-    #[test]
-    #[should_panic = "query only available for named target"]
-    #[cfg(debug_assertions)]
-    fn query_external_debug() {
-        NavigationTarget::from("https://dioxuslabs.com/").query("test");
-    }
-
-    #[test]
-    #[cfg(not(debug_assertions))]
-    fn query_external_release() {
-        assert_eq!(
-            NavigationTarget::from("https://dioxuslabs.com/").query("test"),
-            NavigationTarget::from("https://dioxuslabs.com/")
-        );
-    }
-
-    #[test]
-    fn target_parse_internal() {
-        assert_eq!(
-            "/test".parse::<NavigationTarget>(),
-            Ok(NavigationTarget::Internal("/test".to_string()))
-        );
-    }
-
-    #[test]
-    fn target_parse_external() {
-        assert_eq!(
-            "https://dioxuslabs.com/".parse::<NavigationTarget>(),
-            Ok(NavigationTarget::External(
-                "https://dioxuslabs.com/".to_string()
-            ))
-        )
-    }
-
-    #[test]
-    fn target_from_str_internal() {
-        assert_eq!(
-            NavigationTarget::from("/test"),
-            NavigationTarget::Internal("/test".to_string())
-        );
-    }
-
-    #[test]
-    fn target_from_str_external() {
-        assert_eq!(
-            NavigationTarget::from("https://dioxuslabs.com/"),
-            NavigationTarget::External("https://dioxuslabs.com/".to_string())
-        )
-    }
-
-    #[test]
-    fn target_from_string_internal() {
-        assert_eq!(
-            NavigationTarget::from("/test".to_string()),
-            NavigationTarget::Internal("/test".to_string())
-        );
-    }
-
-    #[test]
-    fn target_from_string_external() {
-        assert_eq!(
-            NavigationTarget::from("https://dioxuslabs.com/".to_string()),
-            NavigationTarget::External("https://dioxuslabs.com/".to_string())
-        )
-    }
-
-    #[test]
-    fn query_from_string() {
-        assert_eq!(
-            Query::from("test".to_string()),
-            Query::Single("test".to_string())
-        )
-    }
-
-    #[test]
-    fn query_from_str() {
-        assert_eq!(Query::from("test"), Query::Single("test".to_string()));
-    }
-
-    #[test]
-    #[cfg(feature = "serde")]
-    fn query_form_vec() {
-        assert_eq!(
-            Query::from(vec![("test", "1234")]),
-            Query::List(vec![("test".to_string(), "1234".to_string())])
-        )
-    }
-}

+ 0 - 97
packages/router-core/src/navigator.rs

@@ -1,97 +0,0 @@
-use futures_channel::mpsc::UnboundedSender;
-
-use crate::{navigation::NavigationTarget, RouterMessage};
-
-/// A [`Navigator`] allowing for programmatic navigation.
-///
-/// The [`Navigator`] is not guaranteed to be able to trigger a navigation. When and if a navigation
-/// is actually handled depends on the UI library.
-pub struct Navigator<I> {
-    sender: UnboundedSender<RouterMessage<I>>,
-}
-
-impl<I> Navigator<I> {
-    /// Go back to the previous location.
-    ///
-    /// Will fail silently if there is no previous location to go to.
-    pub fn go_back(&self) {
-        let _ = self.sender.unbounded_send(RouterMessage::GoBack);
-    }
-
-    /// Go back to the next location.
-    ///
-    /// Will fail silently if there is no next location to go to.
-    pub fn go_forward(&self) {
-        let _ = self.sender.unbounded_send(RouterMessage::GoForward);
-    }
-
-    /// Push a new location.
-    ///
-    /// The previous location will be available to go back to.
-    pub fn push(&self, target: impl Into<NavigationTarget>) {
-        let _ = self
-            .sender
-            .unbounded_send(RouterMessage::Push(target.into()));
-    }
-
-    /// Replace the current location.
-    ///
-    /// The previous location will **not** be available to go back to.
-    pub fn replace(&self, target: impl Into<NavigationTarget>) {
-        let _ = self
-            .sender
-            .unbounded_send(RouterMessage::Replace(target.into()));
-    }
-}
-
-impl<I> From<UnboundedSender<RouterMessage<I>>> for Navigator<I> {
-    fn from(sender: UnboundedSender<RouterMessage<I>>) -> Self {
-        Self { sender }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use futures_channel::mpsc::{unbounded, UnboundedReceiver};
-
-    use super::*;
-
-    fn prepare() -> (Navigator<()>, UnboundedReceiver<RouterMessage<()>>) {
-        let (sender, receiver) = unbounded();
-        (Navigator::from(sender), receiver)
-    }
-
-    #[test]
-    fn go_back() {
-        let (n, mut s) = prepare();
-        n.go_back();
-
-        assert_eq!(s.try_next().unwrap(), Some(RouterMessage::GoBack));
-    }
-
-    #[test]
-    fn go_forward() {
-        let (n, mut s) = prepare();
-        n.go_forward();
-
-        assert_eq!(s.try_next().unwrap(), Some(RouterMessage::GoForward));
-    }
-
-    #[test]
-    fn push() {
-        let (n, mut s) = prepare();
-        let target = NavigationTarget::from("https://dioxuslabs.com/");
-        n.push(target.clone());
-
-        assert_eq!(s.try_next().unwrap(), Some(RouterMessage::Push(target)));
-    }
-
-    #[test]
-    fn replace() {
-        let (n, mut s) = prepare();
-        let target = NavigationTarget::from("https://dioxuslabs.com/");
-        n.replace(target.clone());
-
-        assert_eq!(s.try_next().unwrap(), Some(RouterMessage::Replace(target)));
-    }
-}

+ 0 - 1081
packages/router-core/src/service.rs

@@ -1,1081 +0,0 @@
-use std::{
-    collections::{BTreeMap, HashMap, HashSet},
-    fmt::Debug,
-    sync::{Arc, Weak},
-};
-
-use async_lock::{RwLock, RwLockReadGuard, RwLockWriteGuard};
-use either::Either;
-use futures_channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender};
-use futures_util::StreamExt;
-
-use crate::{
-    history::HistoryProvider,
-    navigation::NavigationTarget,
-    prelude::{
-        FailureExternalNavigation, FailureNamedNavigation, FailureRedirectionLimit, RootIndex,
-    },
-    RouterState,
-};
-
-/// Messages that the [`RouterService`] can handle.
-pub enum RouterMessage<I, R> {
-    /// Subscribe to router update.
-    Subscribe(Arc<I>),
-    /// Navigate to the specified target.
-    Push(NavigationTarget<R>),
-    /// Replace the current location with the specified target.
-    Replace(NavigationTarget),
-    /// Trigger a routing update.
-    Update,
-    /// Navigate to the previous history entry.
-    GoBack,
-    /// Navigate to the next history entry.
-    GoForward,
-}
-
-impl<I: Debug> Debug for RouterMessage<I> {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        match self {
-            Self::Subscribe(arg0) => f.debug_tuple("Subscribe").field(arg0).finish(),
-            Self::Push(arg0) => f.debug_tuple("Push").field(arg0).finish(),
-            Self::Replace(arg0) => f.debug_tuple("Replace").field(arg0).finish(),
-            Self::Update => write!(f, "Update"),
-            Self::GoBack => write!(f, "GoBack"),
-            Self::GoForward => write!(f, "GoForward"),
-        }
-    }
-}
-
-impl<I: PartialEq> PartialEq for RouterMessage<I> {
-    fn eq(&self, other: &Self) -> bool {
-        match (self, other) {
-            (Self::Subscribe(l0), Self::Subscribe(r0)) => l0 == r0,
-            (Self::Push(l0), Self::Push(r0)) => l0 == r0,
-            (Self::Replace(l0), Self::Replace(r0)) => l0 == r0,
-            _ => core::mem::discriminant(self) == core::mem::discriminant(other),
-        }
-    }
-}
-
-impl<I: Eq> Eq for RouterMessage<I> {}
-
-enum NavigationFailure<R: Routable> {
-    External(String),
-    Internal(<R as std::str::FromStr>::Err),
-}
-
-/// A function the router will call after every routing update.
-pub type RoutingCallback<T> = Arc<dyn Fn(&RouterState<T>) -> Option<NavigationTarget>>;
-
-/// A collection of router data that manages all routing functionality.
-pub struct RouterService<T, I>
-where
-    T: Clone,
-    I: Clone + PartialEq + Eq,
-{
-    history: Box<dyn HistoryProvider>,
-    routes: Segment<T>,
-    names: Arc<NameMap>,
-
-    receiver: UnboundedReceiver<RouterMessage<I>>,
-    state: Arc<RwLock<RouterState<T>>>,
-
-    subscribers: Vec<Weak<I>>,
-    subscriber_update: Arc<dyn Fn(I)>,
-    routing_callback: Option<RoutingCallback<T>>,
-
-    failure_external_navigation: ContentAtom<T>,
-    failure_named_navigation: ContentAtom<T>,
-    failure_redirection_limit: ContentAtom<T>,
-}
-
-impl<T, I> RouterService<T, I>
-where
-    T: Clone,
-    I: Clone + PartialEq + Eq + Send + Sync + 'static,
-{
-    /// Create a new [`RouterService`].
-    ///
-    /// # Parameters
-    /// 1. `routes`: The root [`Segment`] the router should handle.
-    /// 2. `history`: A [`HistoryProvider`] to handle the navigation history.
-    /// 3. `subscriber_callback`: A function the rooter can call to update UI integrations.
-    /// 4. `failure_external_navigation`: Content to be displayed when an external navigation fails.
-    /// 5. `failure_named_navigation`: Content to be displayed when a named navigation fails.
-    /// 6. `failure_redirection_limit`: Content to be displayed when the redirection limit is
-    ///    breached.
-    ///
-    /// # Returns
-    /// 1. The [`RouterService`].
-    /// 2. An [`UnboundedSender`] to send [`RouterMessage`]s to the [`RouterService`].
-    /// 3. Access to the [`RouterState`]. **DO NOT WRITE TO THIS!!!** Seriously, **READ ONLY!!!**
-    #[allow(clippy::type_complexity)]
-    pub fn new(
-        routes: Segment<T>,
-        mut history: Box<dyn HistoryProvider>,
-        subscriber_update: Arc<dyn Fn(I)>,
-        routing_callback: Option<RoutingCallback<T>>,
-        failure_external_navigation: ContentAtom<T>,
-        failure_named_navigation: ContentAtom<T>,
-        failure_redirection_limit: ContentAtom<T>,
-    ) -> (
-        Self,
-        UnboundedSender<RouterMessage<I>>,
-        Arc<RwLock<RouterState<T>>>,
-    ) {
-        // index names
-        let names = Arc::new(NamedSegment::from_segment(&routes));
-
-        // create channel
-        let (sender, receiver) = unbounded();
-
-        // initialize history
-        let history_sender = sender.clone();
-        history.updater(Arc::new(move || {
-            let _ = history_sender.unbounded_send(RouterMessage::Update);
-        }));
-        let state = Arc::new(RwLock::new(RouterState {
-            name_map: Arc::clone(&names),
-            ..Default::default()
-        }));
-
-        (
-            Self {
-                history,
-                names,
-                routes,
-                receiver,
-                state: Arc::clone(&state),
-                subscribers: Vec::new(),
-                subscriber_update,
-                routing_callback,
-                failure_external_navigation,
-                failure_named_navigation,
-                failure_redirection_limit,
-            },
-            sender,
-            state,
-        )
-    }
-
-    /// Perform the initial routing.
-    ///
-    /// Call this once, as soon as possible after creating the [`RouterService`]. Do not call this,
-    /// if you are going to call the `run` function.
-    pub fn init(&mut self) {
-        *self.sync_state_write_lock() = self
-            .update_routing()
-            .left_and_then(|state| {
-                if let Some(cb) = &self.routing_callback {
-                    if let Some(nt) = cb(&state) {
-                        self.replace(nt);
-                        return self.update_routing();
-                    }
-                }
-
-                Either::Left(state)
-            })
-            .map_right(|err| self.handle_navigation_failure(&self.sync_state_read_lock(), err))
-            .either_into();
-    }
-
-    /// Handle all messages the router has received and then return.
-    ///
-    /// Call `init` before calling this function.
-    pub fn run_current(&mut self) {
-        let mut state = None;
-        while let Ok(Some(msg)) = self.receiver.try_next() {
-            let current = match self.handle_message(msg) {
-                (_, Some(err)) => Either::Right(err),
-                (true, _) => self.update_routing(),
-                _ => continue,
-            }
-            .left_and_then(|state| {
-                if let Some(cb) = &self.routing_callback {
-                    if let Some(nt) = cb(&state) {
-                        self.replace(nt);
-                        return self.update_routing();
-                    }
-                }
-
-                Either::Left(state)
-            })
-            .map_right(|err| self.handle_navigation_failure(&self.sync_state_read_lock(), err))
-            .either_into();
-            state = Some(current);
-        }
-
-        if let Some(state) = state {
-            *self.sync_state_write_lock() = state;
-        }
-
-        self.update_subscribers();
-    }
-
-    fn sync_state_read_lock(&self) -> RwLockReadGuard<RouterState<T>> {
-        loop {
-            if let Some(s) = self.state.try_read() {
-                return s;
-            }
-        }
-    }
-
-    fn sync_state_write_lock(&mut self) -> RwLockWriteGuard<RouterState<T>> {
-        loop {
-            if let Some(s) = self.state.try_write() {
-                return s;
-            }
-        }
-    }
-
-    /// Handle all routing messages until ended from the outside.
-    pub async fn run(&mut self) {
-        // init (unlike function with same name this is async)
-        {
-            *self.state.write().await = match self.update_routing().left_and_then(|state| {
-                if let Some(cb) = &self.routing_callback {
-                    if let Some(nt) = cb(&state) {
-                        self.replace(nt);
-                        return self.update_routing();
-                    }
-                }
-
-                Either::Left(state)
-            }) {
-                Either::Left(state) => state,
-                Either::Right(err) => {
-                    self.handle_navigation_failure(&*self.state.read().await, err)
-                }
-            };
-        }
-        self.update_subscribers();
-
-        while let Some(msg) = self.receiver.next().await {
-            let state = match self.handle_message(msg) {
-                (_, Some(err)) => Either::Right(err),
-                (true, _) => self.update_routing(),
-                _ => continue,
-            }
-            .left_and_then(|state| {
-                if let Some(cb) = &self.routing_callback {
-                    if let Some(nt) = cb(&state) {
-                        self.replace(nt);
-                        return self.update_routing();
-                    }
-                }
-
-                Either::Left(state)
-            });
-
-            *self.state.write().await = match state {
-                Either::Left(state) => state,
-                Either::Right(err) => {
-                    self.handle_navigation_failure(&*self.state.read().await, err)
-                }
-            };
-
-            self.update_subscribers();
-        }
-    }
-
-    fn handle_navigation_failure(
-        &self,
-        state: &RouterState<T>,
-        err: NavigationFailure,
-    ) -> RouterState<T> {
-        match err {
-            NavigationFailure::External(url) => RouterState {
-                can_go_back: state.can_go_back,
-                can_go_forward: state.can_go_forward,
-                path: state.path.clone(),
-                query: state.query.clone(),
-                prefix: state.prefix.clone(),
-                names: {
-                    let mut r = HashSet::new();
-                    r.insert(Name::of::<FailureExternalNavigation>());
-                    r
-                },
-                parameters: {
-                    let mut r = HashMap::new();
-                    r.insert(Name::of::<FailureExternalNavigation>(), url);
-                    r
-                },
-                name_map: Arc::clone(&state.name_map),
-                content: vec![self.failure_external_navigation.clone()],
-                named_content: BTreeMap::new(),
-            },
-            NavigationFailure::Named(n) => RouterState {
-                can_go_back: state.can_go_back,
-                can_go_forward: state.can_go_forward,
-                path: state.path.clone(),
-                query: state.query.clone(),
-                prefix: state.prefix.clone(),
-                names: {
-                    let mut r = HashSet::new();
-                    r.insert(Name::of::<FailureNamedNavigation>());
-                    r
-                },
-                parameters: {
-                    let mut r = HashMap::new();
-                    r.insert(Name::of::<FailureExternalNavigation>(), n.to_string());
-                    r
-                },
-                name_map: Arc::clone(&state.name_map),
-                content: vec![self.failure_named_navigation.clone()],
-                named_content: BTreeMap::new(),
-            },
-        }
-    }
-
-    #[must_use]
-    fn handle_message(&mut self, msg: RouterMessage<I>) -> (bool, Option<NavigationFailure>) {
-        let failure = match msg {
-            RouterMessage::Subscribe(id) => {
-                self.subscribe(id);
-                return (false, None);
-            }
-            RouterMessage::Push(nt) => self.push(nt),
-            RouterMessage::Replace(nt) => self.replace(nt),
-            RouterMessage::Update => None,
-            RouterMessage::GoBack => {
-                self.history.go_back();
-                None
-            }
-            RouterMessage::GoForward => {
-                self.history.go_forward();
-                None
-            }
-        };
-
-        (true, failure)
-    }
-
-    #[must_use]
-    fn update_routing(&mut self) -> Either<RouterState<T>, NavigationFailure> {
-        for _ in 0..=25 {
-            match self.update_routing_inner() {
-                Either::Left(state) => return Either::Left(state),
-                Either::Right(nt) => {
-                    if let Some(err) = self.replace(nt) {
-                        return Either::Right(err);
-                    }
-                }
-            }
-        }
-
-        #[cfg(debug_assertions)]
-        panic!("reached redirect limit of 25");
-        #[allow(unreachable_code)]
-        Either::Left(RouterState {
-            content: vec![self.failure_redirection_limit.clone()],
-            can_go_back: self.history.can_go_back(),
-            can_go_forward: self.history.can_go_forward(),
-            path: self.history.current_path(),
-            query: self.history.current_query(),
-            prefix: self.history.current_prefix(),
-            name_map: Arc::clone(&self.names),
-            names: {
-                let mut r = HashSet::new();
-                r.insert(Name::of::<FailureRedirectionLimit>());
-                r
-            },
-            ..Default::default()
-        })
-    }
-
-    #[must_use]
-    fn update_routing_inner(&mut self) -> Either<RouterState<T>, NavigationTarget> {
-        // prepare path
-        let mut path = self.history.current_path();
-        path.remove(0);
-        if path.ends_with('/') {
-            path.pop();
-        }
-
-        let values = match path.is_empty() {
-            false => path.split('/').collect::<Vec<_>>(),
-            true => Vec::new(),
-        };
-
-        // add root index name
-        let mut names = HashSet::new();
-        if values.is_empty() {
-            names.insert(Name::of::<RootIndex>());
-        };
-
-        route_segment(
-            &self.routes,
-            &values,
-            RouterState {
-                can_go_back: self.history.can_go_back(),
-                can_go_forward: self.history.can_go_forward(),
-                path: self.history.current_path(),
-                query: self.history.current_query(),
-                prefix: self.history.current_prefix(),
-                name_map: Arc::clone(&self.names),
-                names,
-                ..Default::default()
-            },
-        )
-    }
-
-    fn push(&mut self, target: NavigationTarget) -> Option<NavigationFailure> {
-        match resolve_target(&self.names, &target) {
-            Either::Left(Either::Left(p)) => self.history.push(p),
-            Either::Left(Either::Right(n)) => return Some(NavigationFailure::Named(n)),
-            Either::Right(e) => return self.external(e),
-        }
-
-        None
-    }
-
-    fn replace(&mut self, target: NavigationTarget) -> Option<NavigationFailure> {
-        match resolve_target(&self.names, &target) {
-            Either::Left(Either::Left(p)) => self.history.replace(p),
-            Either::Left(Either::Right(n)) => return Some(NavigationFailure::Named(n)),
-            Either::Right(e) => return self.external(e),
-        }
-
-        None
-    }
-
-    fn external(&mut self, external: String) -> Option<NavigationFailure> {
-        match self.history.external(external.clone()) {
-            true => None,
-            false => Some(NavigationFailure::External(external)),
-        }
-    }
-
-    fn subscribe(&mut self, id: Arc<I>) {
-        self.subscribers.push(Arc::downgrade(&id));
-        (self.subscriber_update)(id.as_ref().clone());
-    }
-
-    fn update_subscribers(&mut self) {
-        let mut previous = Vec::new();
-        self.subscribers.retain(|id| {
-            if let Some(id) = id.upgrade() {
-                if previous.contains(&id) {
-                    false
-                } else {
-                    (self.subscriber_update)(id.as_ref().clone());
-                    previous.push(id);
-                    true
-                }
-            } else {
-                false
-            }
-        });
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    //! The tests for [`RouterService`] 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_path(&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, _, _) = RouterService::<_, 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, _, _) = RouterService::<_, 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, _, _) = RouterService::<_, 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, _, _) = RouterService::<_, 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, _, _) = RouterService::<_, 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, _, _) = RouterService::<_, 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, _, _) = RouterService::<_, 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, _, _) = RouterService::<_, 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, _, _) = RouterService::<_, 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, _) = RouterService::<_, 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, _, _) = RouterService::<_, 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, _, _) = RouterService::<_, 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, _) = RouterService::<_, 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, _, _) = RouterService::<_, 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, _) = RouterService::<_, 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, _, _) = RouterService::<_, 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, _) = RouterService::<_, 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_path(&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, _, _) = RouterService::<_, 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);
-    }
-}

+ 0 - 288
packages/router-core/src/state.rs

@@ -1,288 +0,0 @@
-use std::{
-    collections::{BTreeMap, HashMap, HashSet},
-    sync::Arc,
-};
-
-use either::Either;
-
-use crate::{
-    navigation::NavigationTarget, routes::ContentAtom, segments::NameMap, utils::resolve_target,
-    Name,
-};
-
-/// The current state of the router.
-#[derive(Debug)]
-pub struct RouterState<T: Clone> {
-    /// Whether there is a previous page to navigate back to.
-    ///
-    /// Even if this is [`true`], there might not be a previous page. However, it is nonetheless
-    /// safe to tell the router to go back.
-    pub can_go_back: bool,
-    /// Whether there is a future page to navigate forward to.
-    ///
-    /// Even if this is [`true`], there might not be a future page. However, it is nonetheless safe
-    /// to tell the router to go forward.
-    pub can_go_forward: bool,
-
-    /// The current path.
-    pub path: String,
-    /// The current query.
-    pub query: Option<String>,
-    /// The current prefix.
-    pub prefix: Option<String>,
-
-    /// The names of currently active routes.
-    pub names: HashSet<Name>,
-    /// The current path parameters.
-    pub parameters: HashMap<Name, String>,
-    pub(crate) name_map: Arc<NameMap>,
-
-    /// The current main content.
-    ///
-    /// This should only be used by UI integration crates, and not by applications.
-    pub content: Vec<ContentAtom<T>>,
-    /// The current named content.
-    ///
-    /// This should only be used by UI integration crates, and not by applications.
-    pub named_content: BTreeMap<Name, Vec<ContentAtom<T>>>,
-}
-
-impl<T: Clone> RouterState<T> {
-    /// Get a parameter.
-    ///
-    /// ```rust
-    /// # use dioxus_router_core::{RouterState, Name};
-    /// let mut state = RouterState::<&'static str>::default();
-    /// assert_eq!(state.parameter::<bool>(), None);
-    ///
-    /// // Do not do this! For illustrative purposes only!
-    /// state.parameters.insert(Name::of::<bool>(), String::from("some parameter"));
-    /// assert_eq!(state.parameter::<bool>(), Some("some parameter".to_string()));
-    /// ```
-    pub fn parameter<N: 'static>(&self) -> Option<String> {
-        self.parameters.get(&Name::of::<N>()).cloned()
-    }
-
-    /// Get the `href` for the `target`.
-    pub fn href(&self, target: &NavigationTarget) -> String {
-        match resolve_target(&self.name_map, target) {
-            Either::Left(Either::Left(i)) => match &self.prefix {
-                Some(p) => format!("{p}{i}"),
-                None => i,
-            },
-            Either::Left(Either::Right(n)) => {
-                // the following assert currently cannot trigger, as resolve_target (or more
-                // precisely resolve_name, which is called by resolve_targe) will panic in debug
-                debug_assert!(false, "requested href for unknown name or parameter: {n}");
-                String::new()
-            }
-            Either::Right(e) => e,
-        }
-    }
-
-    /// Check whether the `target` is currently active.
-    ///
-    /// # Normal mode
-    /// 1. For internal targets wrapping an absolute path, the current path has to start with it.
-    /// 2. For internal targets wrapping a relative path, it has to match the last current segment
-    ///    exactly.
-    /// 3. For named targets, the provided name needs to be active.
-    /// 4. For external targets [`false`].
-    ///
-    /// # Exact mode
-    /// 1. For internal targets, the current path must match the wrapped path exactly.
-    /// 2. For named targets, the provided name needs to be active and all parameters need to match
-    ///    exactly.
-    /// 3. For external targets [`false`].
-    pub fn is_at(&self, target: &NavigationTarget, exact: bool) -> bool {
-        match target {
-            NavigationTarget::Internal(i) => {
-                if exact {
-                    i == &self.path
-                } else if i.starts_with('/') {
-                    self.path.starts_with(i)
-                } else if let Some((_, s)) = self.path.rsplit_once('/') {
-                    s == i
-                } else {
-                    false
-                }
-            }
-            NavigationTarget::Named {
-                name,
-                parameters,
-                query: _,
-            } => {
-                if !self.names.contains(name) {
-                    false
-                } else if exact {
-                    for (k, v) in parameters {
-                        match self.parameters.get(k) {
-                            Some(p) if p != v => return false,
-                            None => return false,
-                            _ => {}
-                        }
-                    }
-
-                    true
-                } else {
-                    true
-                }
-            }
-            NavigationTarget::External(_) => false,
-        }
-    }
-}
-
-// manual impl required because derive macro requires default for T unnecessarily
-impl<T: Clone> Default for RouterState<T> {
-    fn default() -> Self {
-        Self {
-            can_go_back: Default::default(),
-            can_go_forward: Default::default(),
-            path: Default::default(),
-            query: Default::default(),
-            prefix: Default::default(),
-            names: Default::default(),
-            parameters: Default::default(),
-            name_map: Default::default(),
-            content: Default::default(),
-            named_content: Default::default(),
-        }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use crate::{navigation::named, prelude::RootIndex};
-
-    use super::*;
-
-    #[test]
-    fn href_internal() {
-        let state = RouterState::<&str> {
-            prefix: Some(String::from("/prefix")),
-            ..Default::default()
-        };
-
-        assert_eq!(state.href(&"/test".into()), String::from("/prefix/test"))
-    }
-
-    #[test]
-    fn href_named() {
-        let state = RouterState::<&str> {
-            name_map: Arc::new(NamedSegment::from_segment(&Segment::<&str>::empty())),
-            prefix: Some(String::from("/prefix")),
-            ..Default::default()
-        };
-
-        assert_eq!(state.href(&named::<RootIndex>()), String::from("/prefix/"))
-    }
-
-    #[test]
-    #[should_panic = "named navigation to unknown name: bool"]
-    #[cfg(debug_assertions)]
-    fn href_named_debug() {
-        let state = RouterState::<&str> {
-            name_map: Arc::new(NamedSegment::from_segment(&Segment::<&str>::empty())),
-            prefix: Some(String::from("/prefix")),
-            ..Default::default()
-        };
-
-        state.href(&named::<bool>());
-    }
-
-    #[test]
-    #[cfg(not(debug_assertions))]
-    fn href_named_release() {
-        let state = RouterState::<&str> {
-            name_map: Arc::new(NamedSegment::from_segment(&Segment::<&str>::empty())),
-            prefix: Some(String::from("/prefix")),
-            ..Default::default()
-        };
-
-        assert_eq!(state.href(&named::<bool>()), String::new())
-    }
-
-    #[test]
-    fn href_external() {
-        let state = RouterState::<&str> {
-            prefix: Some(String::from("/prefix")),
-            ..Default::default()
-        };
-
-        assert_eq!(
-            state.href(&"https://dioxuslabs.com/".into()),
-            String::from("https://dioxuslabs.com/")
-        )
-    }
-
-    #[test]
-    fn is_at_internal_absolute() {
-        let state = test_state();
-
-        assert!(!state.is_at(&"/levels".into(), false));
-        assert!(!state.is_at(&"/levels".into(), true));
-
-        assert!(state.is_at(&"/test".into(), false));
-        assert!(!state.is_at(&"/test".into(), true));
-
-        assert!(state.is_at(&"/test/with/some/nested/levels".into(), false));
-        assert!(state.is_at(&"/test/with/some/nested/levels".into(), true));
-    }
-
-    #[test]
-    fn is_at_internal_relative() {
-        let state = test_state();
-
-        assert!(state.is_at(&"levels".into(), false));
-        assert!(!state.is_at(&"levels".into(), true));
-
-        assert!(!state.is_at(&"test".into(), false));
-        assert!(!state.is_at(&"test".into(), true));
-
-        assert!(!state.is_at(&"test/with/some/nested/levels".into(), false));
-        assert!(!state.is_at(&"test/with/some/nested/levels".into(), true));
-    }
-
-    #[test]
-    fn is_at_named() {
-        let state = test_state();
-
-        assert!(!state.is_at(&named::<RootIndex>(), false));
-        assert!(!state.is_at(&named::<RootIndex>(), true));
-
-        assert!(state.is_at(&named::<bool>(), false));
-        assert!(state.is_at(&named::<bool>(), true));
-
-        assert!(state.is_at(&named::<bool>().parameter::<bool>("test"), false));
-        assert!(state.is_at(&named::<bool>().parameter::<bool>("test"), true));
-
-        assert!(state.is_at(&named::<bool>().parameter::<i8>("test"), false));
-        assert!(!state.is_at(&named::<bool>().parameter::<i8>("test"), true));
-    }
-
-    #[test]
-    fn is_at_external() {
-        let state = test_state();
-
-        assert!(!state.is_at(&"https://dioxuslabs.com/".into(), false));
-        assert!(!state.is_at(&"https://dioxuslabs.com/".into(), true));
-    }
-
-    fn test_state() -> RouterState<&'static str> {
-        RouterState {
-            path: String::from("/test/with/some/nested/levels"),
-            names: {
-                let mut r = HashSet::new();
-                r.insert(Name::of::<bool>());
-                r
-            },
-            parameters: {
-                let mut r = HashMap::new();
-                r.insert(Name::of::<bool>(), String::from("test"));
-                r
-            },
-            ..Default::default()
-        }
-    }
-}

+ 0 - 245
packages/router-core/tests/macro.rs

@@ -1,245 +0,0 @@
-#![allow(non_snake_case)]
-
-use dioxus::prelude::*;
-use dioxus_router_core::*;
-use dioxus_router_macro::*;
-use std::str::FromStr;
-
-#[inline_props]
-fn Route1(cx: Scope, dynamic: String) -> Element {
-    render! {
-        div{
-            "Route1: {dynamic}"
-        }
-    }
-}
-
-#[inline_props]
-fn Route2(cx: Scope) -> Element {
-    render! {
-        div{
-            "Route2"
-        }
-    }
-}
-
-#[inline_props]
-fn Route3(cx: Scope, dynamic: u32) -> Element {
-    render! {
-        div{
-            "Route3: {dynamic}"
-        }
-    }
-}
-
-#[inline_props]
-fn Route4(cx: Scope, number1: u32, number2: u32) -> Element {
-    render! {
-        div{
-            "Route4: {number1} {number2}"
-        }
-    }
-}
-
-#[inline_props]
-fn Route5(cx: Scope, query: String) -> Element {
-    render! {
-        div{
-            "Route5: {query}"
-        }
-    }
-}
-
-#[inline_props]
-fn Route6(cx: Scope, extra: Vec<String>) -> Element {
-    render! {
-        div{
-            "Route5: {extra:?}"
-        }
-    }
-}
-
-#[inline_props]
-fn Nested(cx: Scope, nested: String) -> Element {
-    render! {
-        div{
-            "Nested: {nested:?}"
-        }
-    }
-}
-
-#[rustfmt::skip]
-#[routable]
-#[derive(Clone, Debug, PartialEq)]
-enum Route {
-    #[route("/:dynamic" Route1)]
-    Route1 { dynamic: String },
-    #[route("/:number1/:number2" Route4)]
-    Route4 { number1: u32, number2: u32 },
-    #[nest("/:nested" nested { nested: String } Nested)]
-        #[route("/" Route2)]
-        Route2 {},
-        // #[redirect("/:dynamic/hello_world")]
-        #[route("/:dynamic" Route3)]
-        Route3 { dynamic: u32 },
-    #[end_nest]
-    #[route("/?:query" Route5)]
-    Route5 { query: String },
-    #[route("/:...extra" Route6)]
-    Route6 { extra: Vec<String> },
-}
-
-#[test]
-fn display_works() {
-    let route = Route::Route1 {
-        dynamic: "hello".to_string(),
-    };
-
-    assert_eq!(route.to_string(), "/hello");
-
-    let route = Route::Route3 {
-        nested: "hello_world".to_string(),
-        dynamic: 1234,
-    };
-
-    assert_eq!(route.to_string(), "/hello_world/1234");
-
-    let route = Route::Route1 {
-        dynamic: "hello_world2".to_string(),
-    };
-
-    assert_eq!(route.to_string(), "/hello_world2");
-
-    let route = Route::Route4 {
-        number1: 1234,
-        number2: 5678,
-    };
-
-    assert_eq!(route.to_string(), "/1234/5678");
-
-    let route = Route::Route5 {
-        query: "hello".to_string(),
-    };
-
-    assert_eq!(route.to_string(), "/?hello");
-
-    let route = Route::Route6 {
-        extra: vec!["hello".to_string(), "world".to_string()],
-    };
-
-    assert_eq!(route.to_string(), "/hello/world");
-}
-
-#[test]
-fn from_string_works() {
-    let w = "/hello";
-    assert_eq!(
-        Route::from_str(w),
-        Ok(Route::Route1 {
-            dynamic: "hello".to_string()
-        })
-    );
-    let w = "/hello/";
-    assert_eq!(
-        Route::from_str(w),
-        Ok(Route::Route1 {
-            dynamic: "hello".to_string()
-        })
-    );
-
-    let w = "/hello_world/1234";
-    assert_eq!(
-        Route::from_str(w),
-        Ok(Route::Route3 {
-            nested: "hello_world".to_string(),
-            dynamic: 1234
-        })
-    );
-    let w = "/hello_world/1234/";
-    assert_eq!(
-        Route::from_str(w),
-        Ok(Route::Route3 {
-            nested: "hello_world".to_string(),
-            dynamic: 1234
-        })
-    );
-
-    let w = "/hello_world2";
-    assert_eq!(
-        Route::from_str(w),
-        Ok(Route::Route1 {
-            dynamic: "hello_world2".to_string()
-        })
-    );
-
-    let w = "/?x=1234&y=hello";
-    assert_eq!(
-        Route::from_str(w),
-        Ok(Route::Route5 {
-            query: "x=1234&y=hello".to_string()
-        })
-    );
-
-    let w = "/hello_world/hello_world/hello_world";
-    assert_eq!(
-        Route::from_str(w),
-        Ok(Route::Route6 {
-            extra: vec![
-                "hello_world".to_string(),
-                "hello_world".to_string(),
-                "hello_world".to_string()
-            ]
-        })
-    );
-}
-
-#[test]
-fn round_trip() {
-    // Route1
-    let string = "hello_world2";
-    let route = Route::Route1 {
-        dynamic: string.to_string(),
-    };
-    assert_eq!(Route::from_str(&route.to_string()), Ok(route));
-
-    // Route2
-    for num1 in 0..100 {
-        for num2 in 0..100 {
-            let route = Route::Route3 {
-                nested: format!("number{num1}"),
-                dynamic: num2,
-            };
-            assert_eq!(Route::from_str(&route.to_string()), Ok(route));
-        }
-    }
-
-    // Route3
-    for num1 in 0..100 {
-        for num2 in 0..100 {
-            let route = Route::Route4 {
-                number1: num1,
-                number2: num2,
-            };
-            assert_eq!(Route::from_str(&route.to_string()), Ok(route));
-        }
-    }
-
-    // Route4
-    let string = "x=1234&y=hello";
-    let route = Route::Route5 {
-        query: string.to_string(),
-    };
-    assert_eq!(Route::from_str(&route.to_string()), Ok(route));
-
-    // Route5
-    let route = Route::Route6 {
-        extra: vec![
-            "hello_world".to_string(),
-            "hello_world".to_string(),
-            "hello_world".to_string(),
-        ],
-    };
-    assert_eq!(Route::from_str(&route.to_string()), Ok(route));
-}
-
-fn main() {}

+ 3 - 3
packages/router-macro/src/lib.rs

@@ -190,7 +190,7 @@ impl RouteEnum {
             }
 
             impl std::str::FromStr for #name {
-                type Err = RouteParseError<#error_name>;
+                type Err = dioxus_router::routable::RouteParseError<#error_name>;
 
                 fn from_str(s: &str) -> Result<Self, Self::Err> {
                     let route = s.strip_prefix('/').unwrap_or(s);
@@ -200,7 +200,7 @@ impl RouteEnum {
 
                     #(#tokens)*
 
-                    Err(RouteParseError {
+                    Err(dioxus_router::routable::RouteParseError {
                         attempted_routes: errors,
                     })
                 }
@@ -289,7 +289,7 @@ impl RouteEnum {
         let index_iter = 0..layers.len();
 
         quote! {
-            impl Routable for #name where Self: Clone {
+            impl dioxus_router::routable::Routable for #name where Self: Clone {
                 fn render<'a>(&self, cx: &'a ScopeState, level: usize) -> Element<'a> {
                     let myself = self.clone();
                     match level {

+ 1 - 1
packages/router-macro/src/nest.rs

@@ -90,7 +90,7 @@ impl Nest {
                 }
                 RouteSegment::Dynamic(ident, ty) => {
                     let missing_error = segment.missing_error_name().unwrap();
-                    error_variants.push(quote! { #error_name(<#ty as dioxus_router_core::router::FromRouteSegment>::Err) });
+                    error_variants.push(quote! { #error_name(<#ty as dioxus_router::routable::FromRouteSegment>::Err) });
                     display_match.push(quote! { Self::#error_name(err) => write!(f, "Dynamic segment '({}:{})' did not match: {}", stringify!(#ident), stringify!(#ty), err)? });
                     error_variants.push(quote! { #missing_error });
                     display_match.push(quote! { Self::#missing_error => write!(f, "Dynamic segment '({}:{})' was missing", stringify!(#ident), stringify!(#ty))? });

+ 1 - 1
packages/router-macro/src/query.rs

@@ -14,7 +14,7 @@ impl QuerySegment {
         let ident = &self.ident;
         let ty = &self.ty;
         quote! {
-            let #ident = <#ty as dioxus_router_core::router::FromQuery>::from_query(query);
+            let #ident = <#ty as dioxus_router::routable::FromQuery>::from_query(query);
         }
     }
 

+ 2 - 2
packages/router-macro/src/route.rs

@@ -203,13 +203,13 @@ impl Route {
                 }
                 RouteSegment::Dynamic(ident, ty) => {
                     let missing_error = segment.missing_error_name().unwrap();
-                    error_variants.push(quote! { #error_name(<#ty as dioxus_router_core::router::FromRouteSegment>::Err) });
+                    error_variants.push(quote! { #error_name(<#ty as dioxus_router::routable::FromRouteSegment>::Err) });
                     display_match.push(quote! { Self::#error_name(err) => write!(f, "Dynamic segment '({}:{})' did not match: {}", stringify!(#ident), stringify!(#ty), err)? });
                     error_variants.push(quote! { #missing_error });
                     display_match.push(quote! { Self::#missing_error => write!(f, "Dynamic segment '({}:{})' was missing", stringify!(#ident), stringify!(#ty))? });
                 }
                 RouteSegment::CatchAll(ident, ty) => {
-                    error_variants.push(quote! { #error_name(<#ty as dioxus_router_core::router::FromRouteSegments>::Err) });
+                    error_variants.push(quote! { #error_name(<#ty as dioxus_router::routable::FromRouteSegments>::Err) });
                     display_match.push(quote! { Self::#error_name(err) => write!(f, "Catch-all segment '({}:{})' did not match: {}", stringify!(#ident), stringify!(#ty), err)? });
                 }
             }

+ 2 - 2
packages/router-macro/src/segment.rs

@@ -80,7 +80,7 @@ impl RouteSegment {
                     {
                         let mut segments = segments.clone();
                         let parsed = if let Some(segment) = segments.next() {
-                            <#ty as dioxus_router_core::router::FromRouteSegment>::from_route_segment(segment).map_err(|err| #error_enum_name::#error_enum_varient(#inner_parse_enum::#error_name(err)))
+                            <#ty as dioxus_router::routable::FromRouteSegment>::from_route_segment(segment).map_err(|err| #error_enum_name::#error_enum_varient(#inner_parse_enum::#error_name(err)))
                         } else {
                             Err(#error_enum_name::#error_enum_varient(#inner_parse_enum::#missing_error_name))
                         };
@@ -101,7 +101,7 @@ impl RouteSegment {
                         let parsed = {
                             let mut segments = segments.clone();
                             let segments: Vec<_> = segments.collect();
-                            <#ty as dioxus_router_core::router::FromRouteSegments>::from_route_segments(&segments).map_err(|err| #error_enum_name::#error_enum_varient(#inner_parse_enum::#error_name(err)))
+                            <#ty as dioxus_router::routable::FromRouteSegments>::from_route_segments(&segments).map_err(|err| #error_enum_name::#error_enum_varient(#inner_parse_enum::#error_name(err)))
                         };
                         match parsed {
                             Ok(#name) => {

+ 4 - 7
packages/router/Cargo.toml

@@ -12,19 +12,16 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
 
 [dependencies]
 anyhow = "1.0.66"
-async-lock = "2.7.0"
 dioxus = { path="../dioxus" }
-dioxus-router-core = { path = "../router-core"}
-futures-channel = "0.3.25"
-futures-util = "0.3.25"
+dioxus-router-macro = { path="../router-macro" }
 log = "0.4.17"
+serde = { version = "1.0.163", features = ["derive"] }
 thiserror = "1.0.37"
+url = "2.3.1"
 
 [features]
-regex = ["dioxus-router-core/regex"]
-serde = ["dioxus-router-core/serde"]
 wasm_test = []
-web = ["dioxus-router-core/web"]
+web = []
 
 [dev-dependencies]
 dioxus = { path = "../dioxus" }

+ 9 - 14
packages/router/src/components/default_errors.rs

@@ -1,17 +1,12 @@
-use crate::{components::Link, hooks::use_route};
+use crate::{components::Link, hooks::use_route, navigation::NavigationTarget, routable::Routable};
 use dioxus::prelude::*;
-use dioxus_router_core::prelude::{named, FailureExternalNavigation as FENName, RootIndex};
 
 #[allow(non_snake_case)]
-pub fn FailureExternalNavigation(cx: Scope) -> Element {
-    let state = use_route(cx).expect(
+pub fn FailureExternalNavigation<R: Routable + Clone>(cx: Scope) -> Element {
+    let href = use_route::<R>(cx).expect(
         "`FailureExternalNavigation` can only be mounted by the router itself, \
             since it is not exposed",
     );
-    let href = state.parameter::<FENName>().expect(
-        "`FailureExternalNavigation` cannot be mounted without receiving its parameter, \
-            since it is not exposed",
-    );
 
     render! {
         h1 { "External Navigation Failure!" }
@@ -28,7 +23,7 @@ pub fn FailureExternalNavigation(cx: Scope) -> Element {
 }
 
 #[allow(non_snake_case)]
-pub fn FailureNamedNavigation(cx: Scope) -> Element {
+pub fn FailureNamedNavigation<R: Routable + Clone>(cx: Scope) -> Element {
     render! {
         h1 { "Named Navigation Failure!" }
         p {
@@ -40,15 +35,15 @@ pub fn FailureNamedNavigation(cx: Scope) -> Element {
             "We are sorry for the inconvenience. The link below may help to fix the problem, but "
             "there is no guarantee."
         }
-        Link {
-            target: named::<RootIndex>(),
+        Link::<R> {
+            target: NavigationTarget::External("https://google.com".into()),
             "Click here to try to fix the failure."
         }
     }
 }
 
 #[allow(non_snake_case)]
-pub fn FailureRedirectionLimit(cx: Scope) -> Element {
+pub fn FailureRedirectionLimit<R: Routable + Clone>(cx: Scope) -> Element {
     render! {
         h1 { "Redirection Limit Failure!" }
         p {
@@ -60,8 +55,8 @@ pub fn FailureRedirectionLimit(cx: Scope) -> Element {
             "We are sorry for the inconvenience. The link below may help to fix the problem, but "
             "there is no guarantee."
         }
-        Link {
-            target: named::<RootIndex>(),
+        Link::<R> {
+            target: NavigationTarget::External("https://google.com".into()),
             "Click here to try to fix the failure."
         }
     }

+ 9 - 22
packages/router/src/components/history_buttons.rs

@@ -1,8 +1,7 @@
 use dioxus::prelude::*;
-use dioxus_router_core::RouterMessage;
 use log::error;
 
-use crate::utils::use_router_internal::use_router_internal;
+use crate::{routable::Routable, utils::use_router_internal::use_router_internal};
 
 /// The properties for a [`GoBackButton`] or a [`GoForwardButton`].
 #[derive(Debug, Props)]
@@ -52,11 +51,11 @@ pub struct HistoryButtonProps<'a> {
 /// # );
 /// ```
 #[allow(non_snake_case)]
-pub fn GoBackButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
+pub fn GoBackButton<'a, R: Routable>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
     let HistoryButtonProps { children } = cx.props;
 
     // hook up to router
-    let router = match use_router_internal(cx) {
+    let router = match use_router_internal::<R>(cx) {
         Some(r) => r,
         #[allow(unreachable_code)]
         None => {
@@ -67,20 +66,14 @@ pub fn GoBackButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
             return None;
         }
     };
-    let state = loop {
-        if let Some(state) = router.state.try_read() {
-            break state;
-        }
-    };
-    let sender = router.sender.clone();
 
-    let disabled = !state.can_go_back;
+    let disabled = !router.can_go_back();
 
     render! {
         button {
             disabled: "{disabled}",
             prevent_default: "onclick",
-            onclick: move |_| { let _ = sender.unbounded_send(RouterMessage::GoBack); },
+            onclick: move |_| router.go_back(),
             children
         }
     }
@@ -127,11 +120,11 @@ pub fn GoBackButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
 /// # );
 /// ```
 #[allow(non_snake_case)]
-pub fn GoForwardButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
+pub fn GoForwardButton<'a, R: Routable>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
     let HistoryButtonProps { children } = cx.props;
 
     // hook up to router
-    let router = match use_router_internal(cx) {
+    let router = match use_router_internal::<R>(cx) {
         Some(r) => r,
         #[allow(unreachable_code)]
         None => {
@@ -142,20 +135,14 @@ pub fn GoForwardButton<'a>(cx: Scope<'a, HistoryButtonProps<'a>>) -> Element {
             return None;
         }
     };
-    let state = loop {
-        if let Some(state) = router.state.try_read() {
-            break state;
-        }
-    };
-    let sender = router.sender.clone();
 
-    let disabled = !state.can_go_back;
+    let disabled = !router.can_go_back();
 
     render! {
         button {
             disabled: "{disabled}",
             prevent_default: "onclick",
-            onclick: move |_| { let _ = sender.unbounded_send(RouterMessage::GoForward); },
+            onclick: move |_| router.go_forward(),
             children
         }
     }

+ 20 - 26
packages/router/src/components/link.rs

@@ -1,14 +1,15 @@
 use std::fmt::Debug;
 
 use dioxus::prelude::*;
-use dioxus_router_core::{navigation::NavigationTarget, RouterMessage};
 use log::error;
 
+use crate::navigation::NavigationTarget;
+use crate::routable::Routable;
 use crate::utils::use_router_internal::use_router_internal;
 
 /// The properties for a [`Link`].
 #[derive(Props)]
-pub struct LinkProps<'a> {
+pub struct LinkProps<'a, R: Routable> {
     /// A class to apply to the generate HTML anchor tag if the `target` route is active.
     pub active_class: Option<&'a str>,
     /// The children to render within the generated HTML anchor tag.
@@ -18,11 +19,6 @@ pub struct LinkProps<'a> {
     /// If `active_class` is [`Some`] and the `target` route is active, `active_class` will be
     /// appended at the end of `class`.
     pub class: Option<&'a str>,
-    /// Require the __exact__ `target` to be active, for the [`Link`] to be active.
-    ///
-    /// See [`RouterState::is_at`](dioxus_router_core::RouterState::is_at) for more details.
-    #[props(default)]
-    pub exact: bool,
     /// The id attribute for the generated HTML anchor tag.
     pub id: Option<&'a str>,
     /// When [`true`], the `target` route will be opened in a new tab.
@@ -46,22 +42,21 @@ pub struct LinkProps<'a> {
     pub rel: Option<&'a str>,
     /// The navigation target. Roughly equivalent to the href attribute of an HTML anchor tag.
     #[props(into)]
-    pub target: NavigationTarget,
+    pub target: NavigationTarget<R>,
 }
 
-impl Debug for LinkProps<'_> {
+impl<R: Routable> Debug for LinkProps<'_, R> {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         f.debug_struct("LinkProps")
             .field("active_class", &self.active_class)
             .field("children", &self.children)
             .field("class", &self.class)
-            .field("exact", &self.exact)
             .field("id", &self.id)
             .field("new_tab", &self.new_tab)
             .field("onclick", &self.onclick.as_ref().map(|_| "onclick is set"))
             .field("onclick_only", &self.onclick_only)
             .field("rel", &self.rel)
-            .field("target", &self.target)
+            .field("target", &self.target.to_string())
             .finish()
     }
 }
@@ -130,12 +125,11 @@ impl Debug for LinkProps<'_> {
 /// # );
 /// ```
 #[allow(non_snake_case)]
-pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
+pub fn Link<'a, R: Routable + Clone>(cx: Scope<'a, LinkProps<'a, R>>) -> Element {
     let LinkProps {
         active_class,
         children,
         class,
-        exact,
         id,
         new_tab,
         onclick,
@@ -145,7 +139,7 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
     } = cx.props;
 
     // hook up to router
-    let router = match use_router_internal(cx) {
+    let router = match use_router_internal::<R>(cx) {
         Some(r) => r,
         #[allow(unreachable_code)]
         None => {
@@ -156,19 +150,19 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
             return None;
         }
     };
-    let state = loop {
-        if let Some(state) = router.state.try_read() {
-            break state;
-        }
-    };
-    let sender = router.sender.clone();
 
-    let href = state.href(target);
+    let current_route = router.current();
+    let href = current_route.to_string();
     let ac = active_class
-        .and_then(|active_class| {
-            state
-                .is_at(target, *exact)
-                .then(|| format!(" {active_class}"))
+        .and_then(|active_class| match target {
+            NavigationTarget::Internal(target) => {
+                if href == target.to_string() {
+                    Some(format!(" {active_class}"))
+                } else {
+                    None
+                }
+            }
+            _ => None,
         })
         .unwrap_or_default();
 
@@ -186,7 +180,7 @@ pub fn Link<'a>(cx: Scope<'a, LinkProps<'a>>) -> Element {
     let do_default = onclick.is_none() || !onclick_only;
     let action = move |event| {
         if do_default && is_router_nav {
-            let _ = sender.unbounded_send(RouterMessage::Push(target.clone()));
+            router.push(target.clone());
         }
 
         if let Some(handler) = onclick {

+ 49 - 0
packages/router/src/components/outlet.rs

@@ -0,0 +1,49 @@
+use crate::{contexts::outlet::OutletContext, routable::Routable};
+use dioxus::prelude::*;
+
+/// An outlet for the current content.
+///
+/// Only works as descendant of a component calling [`use_router`], otherwise it will be inactive.
+///
+/// The [`Outlet`] 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__.
+///
+/// [`use_router`]: crate::hooks::use_router
+///
+/// # Panic
+/// - When the [`Outlet`] is not nested within another component calling the [`use_router`] hook,
+///   but only in debug builds.
+///
+/// # Example
+/// ```rust
+/// # use dioxus::prelude::*;
+/// # use dioxus_router::prelude::*;
+/// fn App(cx: Scope) -> Element {
+///     use_router(
+///         &cx,
+///         &|| RouterConfiguration {
+///             synchronous: true, // asynchronicity not needed for doc test
+///             ..Default::default()
+///         },
+///         &|| Segment::content(comp(Content))
+///     );
+///
+///     render! {
+///         h1 { "App" }
+///         Outlet { } // The content component will be rendered here
+///     }
+/// }
+///
+/// fn Content(cx: Scope) -> Element {
+///     render! {
+///         p { "Content" }
+///     }
+/// }
+/// #
+/// # let mut vdom = VirtualDom::new(App);
+/// # let _ = vdom.rebuild();
+/// # assert_eq!(dioxus_ssr::render(&vdom), "<h1>App</h1><p>Content</p>");
+/// ```
+pub fn Outlet<R: Routable + Clone>(cx: Scope) -> Element {
+    OutletContext::render::<R>(cx)
+}

+ 60 - 0
packages/router/src/components/router.rs

@@ -0,0 +1,60 @@
+use dioxus::prelude::*;
+use log::error;
+use std::{cell::RefCell, str::FromStr};
+
+use crate::{
+    history::HistoryProvider,
+    prelude::{outlet::OutletContext, RouterContext},
+    routable::Routable,
+    router_cfg::RouterConfiguration,
+};
+
+/// The props for [`Router`].
+#[derive(Props)]
+pub struct RouterProps<R: Routable> {
+    #[props(into)]
+    initial_url: Option<String>,
+    #[props(default, into)]
+    config: RefCell<Option<RouterConfiguration<R>>>,
+}
+
+impl<R: Routable> PartialEq for RouterProps<R> {
+    fn eq(&self, _: &Self) -> bool {
+        // prevent the router from re-rendering when the initial url or config changes
+        true
+    }
+}
+
+/// A component that renders the current route.
+pub fn Router<R: Routable + Clone, H: HistoryProvider<R> + Default + 'static>(
+    cx: Scope<RouterProps<R>>,
+) -> Element
+where
+    <R as FromStr>::Err: std::fmt::Display,
+{
+    let router = use_context_provider(cx, || {
+        #[allow(unreachable_code, unused_variables)]
+        if let Some(outer) = cx.consume_context::<RouterContext<R>>() {
+            let msg = "Router components should not be nested within each other";
+            error!("{msg}, inner will be inactive and transparent");
+            #[cfg(debug_assertions)]
+            panic!("{}", msg);
+        }
+        let router = RouterContext::new(
+            cx.props.config.take().unwrap_or_default(),
+            cx.schedule_update_any(),
+        );
+        if let Some(initial) = cx.props.initial_url.as_ref() {
+            router.replace(
+                initial
+                    .parse()
+                    .unwrap_or_else(|_| panic!("failed to parse initial url")),
+            );
+        }
+        router
+    });
+
+    use_context_provider(cx, || OutletContext { current_level: 1 });
+
+    router.current().render(cx, 0)
+}

+ 29 - 0
packages/router/src/contexts/outlet.rs

@@ -0,0 +1,29 @@
+use dioxus::prelude::*;
+
+use crate::{hooks::use_route, routable::Routable};
+
+#[derive(Clone)]
+pub(crate) struct OutletContext {
+    pub current_level: usize,
+}
+
+pub(crate) fn use_outlet_context(cx: &ScopeState) -> &OutletContext {
+    let outlet_context = use_context(cx).unwrap();
+    outlet_context
+}
+
+impl OutletContext {
+    pub(crate) fn render<R: Routable + Clone>(cx: &ScopeState) -> Element<'_> {
+        let outlet = use_outlet_context(cx);
+        let current_level = outlet.current_level;
+        cx.provide_context({
+            OutletContext {
+                current_level: current_level + 1,
+            }
+        });
+
+        use_route::<R>(cx)
+            .expect("Outlet must be inside of a router")
+            .render(cx, current_level)
+    }
+}

+ 806 - 9
packages/router/src/contexts/router.rs

@@ -1,12 +1,809 @@
-use std::sync::Arc;
+use std::{
+    collections::HashSet,
+    sync::{Arc, RwLock, RwLockWriteGuard},
+};
 
-use async_lock::RwLock;
-use dioxus::{core::Component, prelude::ScopeId};
-use dioxus_router_core::{RouterMessage, RouterState};
-use futures_channel::mpsc::UnboundedSender;
+use dioxus::prelude::*;
 
-#[derive(Clone)]
-pub(crate) struct RouterContext {
-    pub(crate) state: Arc<RwLock<RouterState<Component>>>,
-    pub(crate) sender: UnboundedSender<RouterMessage<ScopeId>>,
+use crate::{
+    history::HistoryProvider, navigation::NavigationTarget, routable::Routable,
+    router_cfg::RouterConfiguration,
+};
+
+/// 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),
+}
+
+/// A function the router will call after every routing update.
+pub type RoutingCallback<R> = Arc<dyn Fn(RouterContext<R>) -> Option<NavigationTarget<R>>>;
+
+struct MutableRouterState<R>
+where
+    R: Routable,
+{
+    /// Whether there is a previous page to navigate back to.
+    ///
+    /// Even if this is [`true`], there might not be a previous page. However, it is nonetheless
+    /// safe to tell the router to go back.
+    can_go_back: bool,
+    /// Whether there is a future page to navigate forward to.
+    ///
+    /// Even if this is [`true`], there might not be a future page. However, it is nonetheless safe
+    /// to tell the router to go forward.
+    can_go_forward: bool,
+
+    /// The current prefix.
+    prefix: Option<String>,
+
+    history: Box<dyn HistoryProvider<R>>,
+}
+
+/// A collection of router data that manages all routing functionality.
+pub struct RouterContext<R>
+where
+    R: Routable,
+{
+    state: Arc<RwLock<MutableRouterState<R>>>,
+
+    subscribers: Arc<RwLock<HashSet<ScopeId>>>,
+    subscriber_update: Arc<dyn Fn(ScopeId)>,
+    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 RouterContext<R> {
+    fn clone(&self) -> Self {
+        Self {
+            state: self.state.clone(),
+            subscribers: self.subscribers.clone(),
+            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,
+        }
+    }
+}
+
+impl<R> RouterContext<R>
+where
+    R: Routable,
+{
+    pub(crate) fn new(cfg: RouterConfiguration<R>, mark_dirty: Arc<dyn Fn(ScopeId)>) -> Self
+    where
+        R: Clone,
+    {
+        let state = Arc::new(RwLock::new(MutableRouterState {
+            can_go_back: false,
+            can_go_forward: false,
+            prefix: Default::default(),
+            history: cfg.history,
+        }));
+
+        Self {
+            state,
+            subscribers: Arc::new(RwLock::new(HashSet::new())),
+            subscriber_update: mark_dirty,
+
+            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,
+        }
+    }
+
+    /// Check whether there is a previous page to navigate back to.
+    #[must_use]
+    pub fn can_go_back(&self) -> bool {
+        self.state.read().unwrap().can_go_back
+    }
+
+    /// Check whether there is a future page to navigate forward to.
+    #[must_use]
+    pub fn can_go_forward(&self) -> bool {
+        self.state.read().unwrap().can_go_forward
+    }
+
+    /// Go back to the previous location.
+    ///
+    /// 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();
+    }
+
+    /// 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();
+    }
+
+    /// Push a new location.
+    ///
+    /// The previous location will be available to go back to.
+    pub fn push(&self, target: NavigationTarget<R>) -> Option<NavigationFailure<R>> {
+        let mut state = self.state_mut();
+        match target {
+            NavigationTarget::Internal(p) => state.history.push(p),
+            NavigationTarget::External(e) => return self.external(e),
+        }
+
+        self.update_subscribers();
+        None
+    }
+
+    /// Replace the current location.
+    ///
+    /// The previous location will **not** be available to go back to.
+    pub fn replace(&self, target: NavigationTarget<R>) -> Option<NavigationFailure<R>> {
+        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
+    }
+
+    /// The route that is currently active.
+    pub fn current(&self) -> R
+    where
+        R: Clone,
+    {
+        self.state.read().unwrap().history.current_route().clone()
+    }
+
+    /// The prefix that is currently active.
+    pub fn prefix(&self) -> Option<String> {
+        self.state.read().unwrap().prefix.clone()
+    }
+
+    fn external(&self, external: String) -> Option<NavigationFailure<R>> {
+        let mut state = self.state_mut();
+        match state.history.external(external.clone()) {
+            true => None,
+            false => Some(NavigationFailure::External(external)),
+        }
+    }
+
+    fn state_mut(&self) -> RwLockWriteGuard<MutableRouterState<R>> {
+        self.state.write().unwrap()
+    }
+
+    /// Manually subscribe to the current route
+    pub fn subscribe(&self, id: ScopeId) {
+        self.subscribers.write().unwrap().insert(id);
+    }
+
+    /// Manually unsubscribe from the current route
+    pub fn unsubscribe(&self, id: ScopeId) {
+        self.subscribers.write().unwrap().remove(&id);
+    }
+
+    fn update_subscribers(&self) {
+        for &id in self.subscribers.read().unwrap().iter() {
+            (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_path(&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_path(&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);
+//     }
+// }

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

@@ -0,0 +1,230 @@
+use std::str::FromStr;
+
+use crate::routable::Routable;
+
+use super::HistoryProvider;
+
+/// A [`HistoryProvider`] that stores all navigation information in memory.
+pub struct MemoryHistory<R: Routable> {
+    current: Option<R>,
+    history: Vec<R>,
+    future: Vec<R>,
+}
+
+impl<R: Routable> MemoryHistory<R> {
+    /// Create a [`MemoryHistory`] starting at `path`.
+    ///
+    /// ```rust
+    /// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
+    /// let mut history = MemoryHistory::with_initial_path("/some/path").unwrap();
+    /// assert_eq!(history.current_path(), "/some/path");
+    /// assert_eq!(history.can_go_back(), false);
+    /// ```
+    pub fn with_initial_path(path: impl Into<String>) -> Result<Self, <R as FromStr>::Err> {
+        let path = path.into();
+
+        Ok(Self {
+            current: Some(R::from_str(&path)?),
+            ..Default::default()
+        })
+    }
+}
+
+impl<R: Routable> Default for MemoryHistory<R> {
+    fn default() -> Self {
+        Self {
+            current: None,
+            history: Vec::new(),
+            future: Vec::new(),
+        }
+    }
+}
+
+impl<R: Routable> HistoryProvider<R> for MemoryHistory<R> {
+    fn current_route(&self) -> &R {
+        self.current.as_ref().expect("current route is not set")
+    }
+
+    fn can_go_back(&self) -> bool {
+        !self.history.is_empty()
+    }
+
+    fn go_back(&mut self) {
+        if let Some(last) = self.history.pop() {
+            let new = self.current.replace(last);
+            if let Some(new) = new {
+                self.future.push(new);
+            }
+        }
+    }
+
+    fn can_go_forward(&self) -> bool {
+        !self.future.is_empty()
+    }
+
+    fn go_forward(&mut self) {
+        if let Some(next) = self.future.pop() {
+            let old = self.current.replace(next);
+            if let Some(old) = old {
+                self.history.push(old);
+            }
+        }
+    }
+
+    fn push(&mut self, new: R) {
+        if let Some(current) = self.current.take() {
+            self.history.push(current);
+        }
+        self.current = Some(new);
+        self.future.clear();
+    }
+
+    fn replace(&mut self, path: R) {
+        self.current = Some(path);
+    }
+}
+
+// #[cfg(test)]
+// mod tests {
+//     use super::*;
+
+//     #[test]
+//     fn default() {
+//         let mem = MemoryHistory::default();
+//         assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap());
+//         assert_eq!(mem.history, Vec::<String>::new());
+//         assert_eq!(mem.future, Vec::<String>::new());
+//     }
+
+//     #[test]
+//     fn with_initial_path() {
+//         let mem = MemoryHistory::with_initial_path("something").unwrap();
+//         assert_eq!(
+//             mem.current,
+//             Url::parse(&format!("{INITIAL_URL}something")).unwrap()
+//         );
+//         assert_eq!(mem.history, Vec::<String>::new());
+//         assert_eq!(mem.future, Vec::<String>::new());
+//     }
+
+//     #[test]
+//     fn with_initial_path_with_leading_slash() {
+//         let mem = MemoryHistory::with_initial_path("/something").unwrap();
+//         assert_eq!(
+//             mem.current,
+//             Url::parse(&format!("{INITIAL_URL}something")).unwrap()
+//         );
+//         assert_eq!(mem.history, Vec::<String>::new());
+//         assert_eq!(mem.future, Vec::<String>::new());
+//     }
+
+//     #[test]
+//     fn can_go_back() {
+//         let mut mem = MemoryHistory::default();
+//         assert!(!mem.can_go_back());
+
+//         mem.push(String::from("/test"));
+//         assert!(mem.can_go_back());
+//     }
+
+//     #[test]
+//     fn go_back() {
+//         let mut mem = MemoryHistory::default();
+//         mem.push(String::from("/test"));
+//         mem.go_back();
+
+//         assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap());
+//         assert!(mem.history.is_empty());
+//         assert_eq!(mem.future, vec![format!("{INITIAL_URL}test")]);
+//     }
+
+//     #[test]
+//     fn can_go_forward() {
+//         let mut mem = MemoryHistory::default();
+//         assert!(!mem.can_go_forward());
+
+//         mem.push(String::from("/test"));
+//         mem.go_back();
+
+//         assert!(mem.can_go_forward());
+//     }
+
+//     #[test]
+//     fn go_forward() {
+//         let mut mem = MemoryHistory::default();
+//         mem.push(String::from("/test"));
+//         mem.go_back();
+//         mem.go_forward();
+
+//         assert_eq!(
+//             mem.current,
+//             Url::parse(&format!("{INITIAL_URL}test")).unwrap()
+//         );
+//         assert_eq!(mem.history, vec![INITIAL_URL.to_string()]);
+//         assert!(mem.future.is_empty());
+//     }
+
+//     #[test]
+//     fn push() {
+//         let mut mem = MemoryHistory::default();
+//         mem.push(String::from("/test"));
+
+//         assert_eq!(
+//             mem.current,
+//             Url::parse(&format!("{INITIAL_URL}test")).unwrap()
+//         );
+//         assert_eq!(mem.history, vec![INITIAL_URL.to_string()]);
+//         assert!(mem.future.is_empty());
+//     }
+
+//     #[test]
+//     #[should_panic = r#"cannot navigate to paths starting with "//": //test"#]
+//     #[cfg(debug_assertions)]
+//     fn push_debug() {
+//         let mut mem = MemoryHistory::default();
+//         mem.push(String::from("//test"));
+//     }
+
+//     #[test]
+//     #[cfg(not(debug_assertions))]
+//     fn push_release() {
+//         let mut mem = MemoryHistory::default();
+//         mem.push(String::from("//test"));
+
+//         assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap());
+//         assert!(mem.history.is_empty())
+//     }
+
+//     #[test]
+//     fn replace() {
+//         let mut mem = MemoryHistory::default();
+//         mem.push(String::from("/test"));
+//         mem.push(String::from("/other"));
+//         mem.go_back();
+//         mem.replace(String::from("/replace"));
+
+//         assert_eq!(
+//             mem.current,
+//             Url::parse(&format!("{INITIAL_URL}replace")).unwrap()
+//         );
+//         assert_eq!(mem.history, vec![INITIAL_URL.to_string()]);
+//         assert_eq!(mem.future, vec![format!("{INITIAL_URL}other")]);
+//     }
+
+//     #[test]
+//     #[should_panic = r#"cannot navigate to paths starting with "//": //test"#]
+//     #[cfg(debug_assertions)]
+//     fn replace_debug() {
+//         let mut mem = MemoryHistory::default();
+//         mem.replace(String::from("//test"));
+//     }
+
+//     #[test]
+//     #[cfg(not(debug_assertions))]
+//     fn replace_release() {
+//         let mut mem = MemoryHistory::default();
+//         mem.replace(String::from("//test"));
+
+//         assert_eq!(mem.current, Url::parse(INITIAL_URL).unwrap());
+//     }
+// }

+ 10 - 17
packages/router-core/src/history/mod.rs → packages/router/src/history/mod.rs

@@ -21,6 +21,8 @@ mod web_hash;
 #[cfg(feature = "web")]
 pub use web_hash::*;
 
+use crate::routable::Routable;
+
 #[cfg(feature = "web")]
 pub(crate) mod web_scroll;
 
@@ -32,7 +34,7 @@ pub(crate) mod web_scroll;
 /// However, you should document all deviations. Also, make sure the navigation is user-friendly.
 /// The described behaviors are designed to mimic a web browser, which most users should already
 /// know. Deviations might confuse them.
-pub trait HistoryProvider {
+pub trait HistoryProvider<R: Routable> {
     /// Get the path of the current URL.
     ///
     /// **Must start** with `/`. **Must _not_ contain** the prefix.
@@ -46,20 +48,8 @@ pub trait HistoryProvider {
     /// assert_eq!(history.current_path(), "/path");
     /// ```
     #[must_use]
-    fn current_path(&self) -> String;
-    /// Get the query string of the current URL.
-    ///
-    /// **Must _not_** start with `?`.
-    ///
-    /// ```rust
-    /// # use dioxus_router_core::history::{HistoryProvider, MemoryHistory};
-    /// let mut history = MemoryHistory::default();
-    /// assert_eq!(history.current_query(), None);
-    ///
-    /// history.push(String::from("?some=value"));
-    /// assert_eq!(history.current_query(), Some("some=value".to_string()));
-    /// ```
-    fn current_query(&self) -> Option<String>;
+    fn current_route(&self) -> &R;
+
     /// Get the current path prefix of the URL.
     ///
     /// Not all [`HistoryProvider`]s need a prefix feature. It is meant for environments where a
@@ -87,6 +77,7 @@ pub trait HistoryProvider {
     fn can_go_back(&self) -> bool {
         true
     }
+
     /// Go back to a previous page.
     ///
     /// If a [`HistoryProvider`] cannot go to a previous page, it should do nothing. This method
@@ -127,6 +118,7 @@ pub trait HistoryProvider {
     fn can_go_forward(&self) -> bool {
         true
     }
+
     /// Go forward to a future page.
     ///
     /// If a [`HistoryProvider`] cannot go to a previous page, it should do nothing. This method
@@ -162,7 +154,8 @@ pub trait HistoryProvider {
     /// assert_eq!(history.current_path(), "/some-other-page");
     /// assert!(history.can_go_back());
     /// ```
-    fn push(&mut self, path: String);
+    fn push(&mut self, route: R);
+
     /// Replace the current page with another one.
     ///
     /// This should merge the current URL with the `path` parameter (which may also include a query
@@ -178,7 +171,7 @@ pub trait HistoryProvider {
     /// assert_eq!(history.current_path(), "/some-other-page");
     /// assert!(!history.can_go_back());
     /// ```
-    fn replace(&mut self, path: String);
+    fn replace(&mut self, path: R);
 
     /// Navigate to an external URL.
     ///

+ 2 - 2
packages/router-core/src/history/web.rs → packages/router/src/history/web.rs

@@ -23,7 +23,7 @@ use super::{
 /// in the URL. Otherwise, if a router navigation is triggered, the prefix will be added.
 ///
 /// [History API]: https://developer.mozilla.org/en-US/docs/Web/API/History_API
-pub struct WebHistory {
+pub struct WebHistory<R: Serialize + DeserializeOwned> {
     do_scroll_restoration: bool,
     history: History,
     listener_navigation: Option<EventListener>,
@@ -72,7 +72,7 @@ impl WebHistory {
 }
 
 impl HistoryProvider for WebHistory {
-    fn current_path(&self) -> String {
+    fn current_route(&self) -> String {
         let path = self
             .window
             .location()

+ 2 - 2
packages/router-core/src/history/web_hash.rs → packages/router/src/history/web_hash.rs

@@ -21,7 +21,7 @@ const INITIAL_URL: &str = "dioxus-router-core://initial_url.invalid/";
 /// was not, because no browser supports WebAssembly without the [History API].
 ///
 /// [History API]: https://developer.mozilla.org/en-US/docs/Web/API/History_API
-pub struct WebHashHistory {
+pub struct WebHashHistory<R: Serialize + DeserializeOwned> {
     do_scroll_restoration: bool,
     history: History,
     listener_navigation: Option<EventListener>,
@@ -121,7 +121,7 @@ impl WebHashHistory {
 }
 
 impl HistoryProvider for WebHashHistory {
-    fn current_path(&self) -> String {
+    fn current_route(&self) -> String {
         self.url()
             .map(|url| url.path().to_string())
             .unwrap_or(String::from("/"))

+ 0 - 0
packages/router-core/src/history/web_scroll.rs → packages/router/src/history/web_scroll.rs


+ 0 - 71
packages/router/src/hooks/use_navigate.rs

@@ -1,71 +0,0 @@
-use dioxus::prelude::{ScopeId, ScopeState};
-use dioxus_router_core::Navigator;
-
-use crate::utils::use_router_internal::use_router_internal;
-
-/// A hook that allows for programmatic navigation.
-///
-/// # Return values
-/// - [`RouterError::NotInsideRouter`], when the calling component is not nested within another
-///   component calling the [`use_router`] hook.
-/// - Otherwise [`Ok`].
-///
-/// [`use_router`]: crate::hooks::use_router
-///
-/// # Panic
-/// - When the calling component is not nested within another component calling the [`use_router`]
-///   hook, but only in debug builds.
-///
-/// # Example
-/// ```rust
-/// # use dioxus::prelude::*;
-/// # use dioxus_router::prelude::*;
-/// fn App(cx: Scope) -> Element {
-///     let (state, _) = use_router(
-///         &cx,
-///         &|| RouterConfiguration {
-///             synchronous: true, // asynchronicity not needed for doc test
-///             ..Default::default()
-///         },
-///         &|| Segment::content(comp(Redirect)).fixed("content", comp(Content))
-///     );
-///
-///     render! {
-///         h1 { "App" }
-///         Outlet { }
-///     }
-/// }
-///
-/// fn Redirect(cx: Scope) -> Element {
-///     let nav = use_navigate(&cx)?;
-///     nav.push("/content");
-///     render! { () }
-/// }
-///
-/// fn Content(cx: Scope) -> Element {
-///     render! {
-///         p { "Content" }
-///     }
-/// }
-/// #
-/// # let mut vdom = VirtualDom::new(App);
-/// #
-/// # // first render with Redirect component
-/// # let _ = vdom.rebuild();
-/// # assert_eq!(dioxus_ssr::render(&vdom), "<h1>App</h1>");
-/// #
-/// # // second render with Content component
-/// # let _ = vdom.rebuild();
-/// # assert_eq!(dioxus_ssr::render(&vdom), "<h1>App</h1><p>Content</p>");
-/// ```
-pub fn use_navigate(cx: &ScopeState) -> Option<Navigator<ScopeId>> {
-    match use_router_internal(cx) {
-        Some(r) => Some(r.sender.clone().into()),
-        None => {
-            #[cfg(debug_assertions)]
-            panic!("`use_navigate` must have access to a parent router");
-            #[allow(unreachable_code)]
-            None
-        }
-    }
-}

+ 4 - 10
packages/router/src/hooks/use_route.rs

@@ -1,8 +1,6 @@
-use async_lock::RwLockReadGuard;
-use dioxus::{core::Component, prelude::ScopeState};
-use dioxus_router_core::RouterState;
+use dioxus::prelude::ScopeState;
 
-use crate::utils::use_router_internal::use_router_internal;
+use crate::{routable::Routable, utils::use_router_internal::use_router_internal};
 
 /// A hook that provides access to information about the current routing location.
 ///
@@ -56,13 +54,9 @@ use crate::utils::use_router_internal::use_router_internal;
 /// ```
 ///
 /// [`use_router`]: crate::hooks::use_router
-pub fn use_route(cx: &ScopeState) -> Option<RwLockReadGuard<RouterState<Component>>> {
+pub fn use_route<R: Routable + Clone>(cx: &ScopeState) -> Option<R> {
     match use_router_internal(cx) {
-        Some(r) => loop {
-            if let Some(s) = r.state.try_read() {
-                break Some(s);
-            }
-        },
+        Some(r) => Some(r.current()),
         None => {
             #[cfg(debug_assertions)]
             panic!("`use_route` must have access to a parent router");

+ 7 - 195
packages/router/src/hooks/use_router.rs

@@ -1,200 +1,12 @@
-use async_lock::RwLockReadGuard;
-use dioxus::{core::Component, prelude::*};
-use dioxus_router_core::{
-    history::{HistoryProvider, MemoryHistory},
-    routes::{ContentAtom, Segment},
-    Navigator, RouterService, RouterState, RoutingCallback,
-};
-use log::error;
+use dioxus::prelude::ScopeState;
 
 use crate::{
-    contexts::router::RouterContext,
-    prelude::{
-        comp,
-        default_errors::{
-            FailureExternalNavigation, FailureNamedNavigation, FailureRedirectionLimit,
-        },
-    },
+    prelude::RouterContext, routable::Routable, utils::use_router_internal::use_router_internal,
 };
 
-/// The basic building block required for all other router components and hooks.
-///
-/// This manages a [`dioxus_router_core::RouterService`], which in turn is required for basically
-/// all router functionality. All other components and hooks provided by [`dioxus_router`](crate)
-/// will only work as/in components nested within a component calling [`use_router`].
-///
-/// Components calling [`use_router`] should not be nested within each other.
-///
-/// # Return values
-/// This hook returns the current router state and a navigator. For more information about the
-/// state, see the [`use_route`](crate::hooks::use_route) hook. For more information about the
-/// [`Navigator`], see its own documentation and the [`use_navigate`](crate::hooks::use_navigate)
-/// hook.
-///
-/// # Panic
-/// - When used within a component, that is nested inside another component calling [`use_router`],
-///   but only in debug builds.
-///
-/// # Example
-/// ```rust
-/// # use dioxus::prelude::*;
-/// # use dioxus_router::prelude::*;
-/// fn App(cx: Scope) -> Element {
-///     let (_, _) = use_router(
-///         &cx,
-///         &|| RouterConfiguration {
-///             synchronous: true, // asynchronicity not needed for doc test
-///             ..Default::default()
-///         },
-///         &|| Segment::content(comp(Content))
-///     );
-///
-///     render! {
-///         h1 { "App" }
-///         Outlet { }
-///     }
-/// }
-///
-/// fn Content(cx: Scope) -> Element {
-///     render! {
-///         p { "Some content" }
-///     }
-/// }
-/// # let mut vdom = VirtualDom::new(App);
-/// # let _ = vdom.rebuild();
-/// # assert_eq!(dioxus_ssr::render(&vdom), "<h1>App</h1><p>Some content</p>");
-/// ```
-pub fn use_router<'a>(
-    cx: &'a ScopeState,
-    cfg: &dyn Fn() -> RouterConfiguration,
-    content: &dyn Fn() -> Segment<Component>,
-) -> (
-    RwLockReadGuard<'a, RouterState<Component>>,
-    Navigator<ScopeId>,
-) {
-    let (service, state, sender) = cx.use_hook(|| {
-        #[allow(unreachable_code, unused_variables)]
-        if let Some(outer) = cx.consume_context::<RouterContext>() {
-            let msg = "components using `use_router` should not be nested within each other";
-            error!("{msg}, inner will be inactive and transparent");
-            #[cfg(debug_assertions)]
-            panic!("{}", msg);
-            return (None, outer.state, outer.sender);
-        }
-
-        let cfg = cfg();
-        let content = content();
-
-        let (mut service, sender, state) = RouterService::new(
-            content,
-            cfg.history,
-            cx.schedule_update_any(),
-            cfg.on_update,
-            cfg.failure_external_navigation,
-            cfg.failure_named_navigation,
-            cfg.failure_redirection_limit,
-        );
-
-        cx.provide_context(RouterContext {
-            state: state.clone(),
-            sender: sender.clone(),
-        });
-
-        (
-            if cfg.synchronous {
-                service.init();
-                Some(service)
-            } else {
-                cx.spawn(async move { service.run().await });
-                None
-            },
-            state,
-            sender,
-        )
-    });
-
-    if let Some(service) = service {
-        service.run_current();
-    }
-
-    (
-        loop {
-            if let Some(state) = state.try_read() {
-                break state;
-            }
-        },
-        sender.clone().into(),
-    )
-}
-
-/// Global configuration options for the router.
-///
-/// This implements [`Default`], so you can use it like this:
-/// ```rust,no_run
-/// # use dioxus_router::prelude::RouterConfiguration;
-/// let cfg = RouterConfiguration {
-///     synchronous: false,
-///     ..Default::default()
-/// };
-/// ```
-pub struct RouterConfiguration {
-    /// 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_core::prelude::FailureExternalNavigation`].
-    pub failure_external_navigation: ContentAtom<Component>,
-    /// 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_core::prelude::FailureNamedNavigation`].
-    pub failure_named_navigation: ContentAtom<Component>,
-    /// 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_core::prelude::FailureRedirectionLimit`].
-    pub failure_redirection_limit: ContentAtom<Component>,
-    /// The [`HistoryProvider`] the router should use.
-    ///
-    /// Defaults to a default [`MemoryHistory`].
-    pub history: Box<dyn HistoryProvider>,
-    /// A function to be called whenever the routing is updated.
-    ///
-    /// The callback is invoked after the routing is updated, but before components and hooks are
-    /// updated.
-    ///
-    /// 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.
-    ///
-    /// The callback is called no more than once per rerouting. It will not be called if a
-    /// navigation failure occurs.
-    ///
-    /// Defaults to [`None`].
-    ///
-    /// [`NavigationTarget`]: dioxus_router_core::navigation::NavigationTarget
-    pub on_update: Option<RoutingCallback<Component>>,
-    /// Whether the router should run in synchronous mode.
-    ///
-    /// If [`true`], the router will only update its state whenever the component using the
-    /// [`use_router`] hook rerenders. If [`false`], an asynchronous task is launched and the router
-    /// will update whenever it receives new input.
-    ///
-    /// Defaults to [`false`].
-    pub synchronous: bool,
-}
-
-impl Default for RouterConfiguration {
-    fn default() -> Self {
-        Self {
-            failure_external_navigation: comp(FailureExternalNavigation),
-            failure_named_navigation: comp(FailureNamedNavigation),
-            failure_redirection_limit: comp(FailureRedirectionLimit),
-            history: Box::<MemoryHistory>::default(),
-            on_update: None,
-            synchronous: false,
-        }
-    }
+/// A hook that provides access to information about the router
+pub fn use_router<R: Routable + Clone>(cx: &ScopeState) -> &RouterContext<R> {
+    use_router_internal(cx)
+        .as_ref()
+        .expect("use_route must have access to a router")
 }

+ 14 - 14
packages/router/src/lib.rs

@@ -1,6 +1,10 @@
 #![doc = include_str!("../README.md")]
 // cannot use forbid, because props derive macro generates #[allow(missing_docs)]
 #![deny(missing_docs)]
+#![allow(non_snake_case)]
+
+pub mod navigation;
+pub mod routable;
 
 /// Components interacting with the router.
 pub mod components {
@@ -14,19 +18,23 @@ pub mod components {
 
     mod outlet;
     pub use outlet::*;
+
+    mod router;
+    pub use router::*;
 }
 
 mod contexts {
+    pub(crate) mod outlet;
     pub(crate) mod router;
+    pub use router::*;
 }
 
-pub use dioxus_router_core::history;
+mod router_cfg;
+
+pub mod history;
 
 /// Hooks for interacting with the router in components.
 pub mod hooks {
-    mod use_navigate;
-    pub use use_navigate::*;
-
     mod use_router;
     pub use use_router::*;
 
@@ -36,18 +44,10 @@ pub mod hooks {
 
 /// A collection of useful items most applications might need.
 pub mod prelude {
-    pub use dioxus_router_core::prelude::*;
-
     pub use crate::components::*;
+    pub use crate::contexts::*;
     pub use crate::hooks::*;
-
-    /// Wrap a [`Component`](dioxus::core::Component) inside a [`ContentAtom`].
-    ///
-    /// This is purely a convenience function.
-    #[must_use]
-    pub fn comp(component: dioxus::core::Component) -> ContentAtom<dioxus::core::Component> {
-        ContentAtom(component)
-    }
+    pub use dioxus_router_macro::routable;
 }
 
 mod utils {

+ 69 - 0
packages/router/src/navigation.rs

@@ -0,0 +1,69 @@
+//! Types pertaining to navigation.
+
+use std::{fmt::Display, str::FromStr};
+
+use url::{ParseError, Url};
+
+use crate::routable::Routable;
+
+/// A target for the router to navigate to.
+#[derive(Clone, Debug, PartialEq, Eq)]
+pub enum NavigationTarget<R: Routable> {
+    /// An internal path that the router can navigate to by itself.
+    ///
+    /// ```rust
+    /// # use dioxus_router_core::navigation::NavigationTarget;
+    /// let explicit = NavigationTarget::Internal(String::from("/internal"));
+    /// let implicit: NavigationTarget = "/internal".into();
+    /// assert_eq!(explicit, implicit);
+    /// ```
+    Internal(R),
+    /// An external target that the router doesn't control.
+    ///
+    /// ```rust
+    /// # use dioxus_router_core::navigation::NavigationTarget;
+    /// let explicit = NavigationTarget::External(String::from("https://dioxuslabs.com/"));
+    /// let implicit: NavigationTarget = "https://dioxuslabs.com/".into();
+    /// assert_eq!(explicit, implicit);
+    /// ```
+    External(String),
+}
+
+impl<R: Routable> From<R> for NavigationTarget<R> {
+    fn from(value: R) -> Self {
+        Self::Internal(value)
+    }
+}
+
+impl<R: Routable> Display for NavigationTarget<R> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        match self {
+            NavigationTarget::Internal(r) => write!(f, "{}", r),
+            NavigationTarget::External(s) => write!(f, "{}", s),
+        }
+    }
+}
+
+/// An error that can occur when parsing a [`NavigationTarget`].
+pub enum NavigationTargetParseError<R: Routable> {
+    /// A URL that is not valid.
+    InvalidUrl(ParseError),
+    /// An internal URL that is not valid.
+    InvalidInternalURL(<R as FromStr>::Err),
+}
+
+impl<R: Routable> FromStr for NavigationTarget<R> {
+    type Err = NavigationTargetParseError<R>;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        match Url::parse(s) {
+            Ok(_) => Ok(Self::External(s.to_string())),
+            Err(ParseError::RelativeUrlWithoutBase) => {
+                Ok(Self::Internal(R::from_str(s).map_err(|e| {
+                    NavigationTargetParseError::InvalidInternalURL(e)
+                })?))
+            }
+            Err(e) => Err(NavigationTargetParseError::InvalidUrl(e)),
+        }
+    }
+}

+ 18 - 102
packages/router-core/src/router.rs → packages/router/src/routable.rs

@@ -1,11 +1,14 @@
+//! # Routable
+
 #![allow(non_snake_case)]
-use crate::history::HistoryProvider;
 use dioxus::prelude::*;
 
-use std::{cell::RefCell, rc::Rc, str::FromStr, sync::Arc};
+use std::str::FromStr;
 
+/// An error that occurs when parsing a route
 #[derive(Debug, PartialEq)]
 pub struct RouteParseError<E: std::fmt::Display> {
+    /// The attempted routes that failed to match
     pub attempted_routes: Vec<E>,
 }
 
@@ -19,39 +22,9 @@ impl<E: std::fmt::Display> std::fmt::Display for RouteParseError<E> {
     }
 }
 
-#[derive(Clone)]
-pub struct Router {
-    subscribers: Rc<RefCell<Vec<ScopeId>>>,
-    update_any: Arc<dyn Fn(ScopeId)>,
-    history: Rc<dyn HistoryProvider>,
-    route: Rc<RefCell<Option<Rc<dyn RouteRenderable>>>>,
-}
-
-impl Router {
-    fn set_route<R: Routable + 'static>(&self, route: R)
-    where
-        R::Err: std::fmt::Display,
-    {
-        *self.route.borrow_mut() = Some(Rc::new(route));
-        for subscriber in self.subscribers.borrow().iter() {
-            (self.update_any)(*subscriber);
-        }
-    }
-}
-
-fn use_router(cx: &ScopeState) -> &Router {
-    use_context(cx).unwrap()
-}
-
-fn use_route(cx: &ScopeState) -> Rc<dyn RouteRenderable> {
-    let router = use_router(cx);
-    cx.use_hook(|| {
-        router.subscribers.borrow_mut().push(cx.scope_id());
-    });
-    router.route.borrow().clone().unwrap()
-}
-
+/// Something that can be created from a query string
 pub trait FromQuery {
+    /// Create an instance of `Self` from a query string
     fn from_query(query: &str) -> Self;
 }
 
@@ -61,9 +34,12 @@ impl<T: for<'a> From<&'a str>> FromQuery for T {
     }
 }
 
+/// Something that can be created from a route segment
 pub trait FromRouteSegment: Sized {
+    /// The error that can occur when parsing a route segment
     type Err;
 
+    /// Create an instance of `Self` from a route segment
     fn from_route_segment(route: &str) -> Result<Self, Self::Err>;
 }
 
@@ -78,7 +54,9 @@ where
     }
 }
 
+/// Something that can be converted to route segments
 pub trait ToRouteSegments {
+    /// Display the route segments
     fn display_route_segements(self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result;
 }
 
@@ -95,9 +73,12 @@ where
     }
 }
 
+/// Something that can be created from route segments
 pub trait FromRouteSegments: Sized {
+    /// The error that can occur when parsing route segments
     type Err;
 
+    /// Create an instance of `Self` from route segments
     fn from_route_segments(segments: &[&str]) -> Result<Self, Self::Err>;
 }
 
@@ -112,14 +93,9 @@ impl<I: std::iter::FromIterator<String>> FromRouteSegments for I {
     }
 }
 
-#[derive(Props, PartialEq)]
-pub struct RouterProps {
-    pub current_route: String,
-}
-pub trait Routable: std::fmt::Display + std::str::FromStr + 'static
-where
-    <Self as FromStr>::Err: std::fmt::Display,
-{
+/// Something that can be routed to
+pub trait Routable: std::fmt::Display + std::str::FromStr + 'static {
+    /// Render the route at the given level
     fn render<'a>(&self, cx: &'a ScopeState, level: usize) -> Element<'a>;
 }
 
@@ -148,63 +124,3 @@ where
         self.render(cx, level)
     }
 }
-
-#[derive(Clone)]
-struct OutletContext {
-    current_level: usize,
-}
-
-fn use_outlet_context(cx: &ScopeState) -> &OutletContext {
-    let outlet_context = use_context(cx).unwrap();
-    outlet_context
-}
-
-impl OutletContext {
-    fn render(cx: &ScopeState) -> Element<'_> {
-        let outlet = use_outlet_context(cx);
-        let current_level = outlet.current_level;
-        cx.provide_context({
-            OutletContext {
-                current_level: current_level + 1,
-            }
-        });
-
-        use_route(cx).render(cx, current_level)
-    }
-}
-
-pub fn Outlet(cx: Scope) -> Element {
-    OutletContext::render(cx)
-}
-
-pub fn Router<R: Routable, H: HistoryProvider + Default + 'static>(
-    cx: Scope<RouterProps>,
-) -> Element
-where
-    <R as FromStr>::Err: std::fmt::Display,
-{
-    let current_route = R::from_str(&cx.props.current_route);
-    let router = use_context_provider(cx, || Router {
-        subscribers: Rc::default(),
-        update_any: cx.schedule_update_any(),
-        history: Rc::<H>::default(),
-        route: Rc::new(RefCell::new(None)),
-    });
-
-    use_context_provider(cx, || OutletContext { current_level: 1 });
-
-    match current_route {
-        Ok(current_route) => {
-            router.set_route(current_route);
-
-            router.route.borrow().as_ref().unwrap().render(cx, 0)
-        }
-        Err(err) => {
-            render! {
-                pre {
-                    "{err}"
-                }
-            }
-        }
-    }
-}

+ 71 - 0
packages/router/src/router_cfg.rs

@@ -0,0 +1,71 @@
+use crate::contexts::router::RoutingCallback;
+use crate::history::{HistoryProvider, MemoryHistory};
+use crate::routable::Routable;
+use dioxus::prelude::*;
+
+use crate::prelude::default_errors::{
+    FailureExternalNavigation, FailureNamedNavigation, FailureRedirectionLimit,
+};
+
+/// Global configuration options for the router.
+///
+/// This implements [`Default`], so you can use it like this:
+/// ```rust,no_run
+/// # use dioxus_router::prelude::RouterConfiguration;
+/// let cfg = RouterConfiguration {
+///     synchronous: false,
+///     ..Default::default()
+/// };
+/// ```
+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_core::prelude::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_core::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_core::prelude::FailureRedirectionLimit`].
+    pub failure_redirection_limit: fn(Scope) -> Element,
+    /// The [`HistoryProvider`] the router should use.
+    ///
+    /// Defaults to a default [`MemoryHistory`].
+    pub history: Box<dyn HistoryProvider<R>>,
+    /// A function to be called whenever the routing is updated.
+    ///
+    /// The callback is invoked after the routing is updated, but before components and hooks are
+    /// updated.
+    ///
+    /// 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.
+    ///
+    /// The callback is called no more than once per rerouting. It will not be called if a
+    /// navigation failure occurs.
+    ///
+    /// Defaults to [`None`].
+    ///
+    /// [`NavigationTarget`]: dioxus_router_core::navigation::NavigationTarget
+    pub on_update: Option<RoutingCallback<R>>,
+}
+
+impl<R: Routable + Clone> Default for RouterConfiguration<R> {
+    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,
+        }
+    }
+}

+ 0 - 0
packages/router-core/src/utils/sitemap.rs → packages/router/src/utils/sitemap.rs


+ 20 - 14
packages/router/src/utils/use_router_internal.rs

@@ -1,9 +1,6 @@
-use std::sync::Arc;
+use dioxus::prelude::{ScopeId, ScopeState};
 
-use dioxus::prelude::ScopeState;
-use dioxus_router_core::RouterMessage;
-
-use crate::contexts::router::RouterContext;
+use crate::{contexts::router::RouterContext, routable::Routable};
 
 /// A private hook to subscribe to the router.
 ///
@@ -15,16 +12,25 @@ use crate::contexts::router::RouterContext;
 /// - Otherwise [`Some`].
 ///
 /// [`use_router`]: crate::hooks::use_router
-pub(crate) fn use_router_internal(cx: &ScopeState) -> &mut Option<RouterContext> {
-    let id = cx.use_hook(|| Arc::new(cx.scope_id()));
+pub(crate) fn use_router_internal<R: Routable>(cx: &ScopeState) -> &Option<RouterContext<R>> {
+    let inner = cx.use_hook(|| {
+        let router = cx.consume_context::<RouterContext<R>>()?;
+
+        let id = cx.scope_id();
+        router.subscribe(id);
 
-    cx.use_hook(|| {
-        let router = cx.consume_context::<RouterContext>()?;
+        Some(Subscription { router, id })
+    });
+    cx.use_hook(|| inner.as_ref().map(|s| s.router.clone()))
+}
 
-        let _ = router
-            .sender
-            .unbounded_send(RouterMessage::Subscribe(id.clone()));
+struct Subscription<R: Routable> {
+    router: RouterContext<R>,
+    id: ScopeId,
+}
 
-        Some(router)
-    })
+impl<R: Routable> Drop for Subscription<R> {
+    fn drop(&mut self) {
+        self.router.unsubscribe(self.id);
+    }
 }