Browse Source

Move the history provider into the context (#3048)

* move history providers into a separate crate

* start moving route providers into the renderers

* clean up intoroutable

* remove into routable

* fix router tests

* Provide history providers in each renderer

* implement nested routers

* move the lens out of the history crate

* re-export dioxus history trait in the prelude

* also re-export the history function

* fix history doctests

* some light cleanups

---------

Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
Evan Almloff 7 months ago
parent
commit
281087469a
48 changed files with 871 additions and 1415 deletions
  1. 16 9
      Cargo.lock
  2. 11 10
      Cargo.toml
  3. 20 5
      examples/meta.rs
  4. 1 0
      packages/desktop/Cargo.toml
  5. 3 0
      packages/desktop/src/webview.rs
  6. 2 1
      packages/dioxus-lib/Cargo.toml
  7. 7 0
      packages/dioxus-lib/src/lib.rs
  8. 6 5
      packages/dioxus/Cargo.toml
  9. 8 0
      packages/dioxus/src/lib.rs
  10. 19 24
      packages/document/src/document.rs
  11. 11 0
      packages/history/Cargo.toml
  12. 54 195
      packages/history/src/lib.rs
  13. 100 0
      packages/history/src/memory.rs
  14. 1 0
      packages/liveview/Cargo.toml
  15. 27 18
      packages/liveview/src/document.rs
  16. 51 104
      packages/liveview/src/history.rs
  17. 2 1
      packages/liveview/src/lib.rs
  18. 4 4
      packages/liveview/src/pool.rs
  19. 0 1
      packages/liveview/src/query.rs
  20. 17 1
      packages/router-macro/src/route.rs
  21. 1 1
      packages/router-macro/src/route_tree.rs
  22. 2 21
      packages/router/Cargo.toml
  23. 0 204
      packages/router/examples/simple_routes.rs
  24. 68 0
      packages/router/src/components/child_router.rs
  25. 20 0
      packages/router/src/components/history_provider.rs
  26. 8 78
      packages/router/src/components/link.rs
  27. 3 2
      packages/router/src/components/outlet.rs
  28. 1 4
      packages/router/src/components/router.rs
  29. 6 3
      packages/router/src/contexts/navigator.rs
  30. 87 143
      packages/router/src/contexts/router.rs
  31. 0 103
      packages/router/src/history/memory.rs
  32. 0 211
      packages/router/src/history/web_hash.rs
  33. 0 42
      packages/router/src/history/web_history.rs
  34. 0 22
      packages/router/src/history/web_scroll.rs
  35. 10 4
      packages/router/src/lib.rs
  36. 43 2
      packages/router/src/navigation.rs
  37. 9 84
      packages/router/src/router_cfg.rs
  38. 1 13
      packages/router/src/utils/use_router_internal.rs
  39. 90 4
      packages/router/tests/via_ssr/link.rs
  40. 7 4
      packages/router/tests/via_ssr/outlet.rs
  41. 6 5
      packages/router/tests/via_ssr/redirect.rs
  42. 7 4
      packages/router/tests/via_ssr/without_index.rs
  43. 3 3
      packages/static-generation/Cargo.toml
  44. 4 0
      packages/web/Cargo.toml
  45. 8 1
      packages/web/src/document.rs
  46. 97 79
      packages/web/src/history/mod.rs
  47. 28 0
      packages/web/src/history/scroll.rs
  48. 2 0
      packages/web/src/lib.rs

+ 16 - 9
Cargo.lock

@@ -3169,6 +3169,7 @@ dependencies = [
  "dioxus-devtools",
  "dioxus-document",
  "dioxus-fullstack",
+ "dioxus-history",
  "dioxus-hooks",
  "dioxus-html",
  "dioxus-liveview",
@@ -3374,6 +3375,7 @@ dependencies = [
  "dioxus-core",
  "dioxus-devtools",
  "dioxus-document",
+ "dioxus-history",
  "dioxus-hooks",
  "dioxus-html",
  "dioxus-interpreter-js",
@@ -3530,6 +3532,15 @@ dependencies = [
  "web-sys",
 ]
 
+[[package]]
+name = "dioxus-history"
+version = "0.6.0-alpha.3"
+dependencies = [
+ "dioxus",
+ "dioxus-core",
+ "tracing",
+]
+
 [[package]]
 name = "dioxus-hooks"
 version = "0.6.0-alpha.3"
@@ -3628,6 +3639,7 @@ dependencies = [
  "dioxus-core",
  "dioxus-core-macro",
  "dioxus-document",
+ "dioxus-history",
  "dioxus-hooks",
  "dioxus-html",
  "dioxus-rsx",
@@ -3644,6 +3656,7 @@ dependencies = [
  "dioxus-core",
  "dioxus-devtools",
  "dioxus-document",
+ "dioxus-history",
  "dioxus-html",
  "dioxus-interpreter-js",
  "futures-channel",
@@ -3733,26 +3746,18 @@ dependencies = [
  "criterion",
  "dioxus",
  "dioxus-cli-config",
- "dioxus-fullstack",
+ "dioxus-history",
  "dioxus-lib",
- "dioxus-liveview",
- "dioxus-router",
  "dioxus-router-macro",
  "dioxus-ssr",
  "gloo",
- "gloo-utils 0.1.7",
- "http 1.1.0",
- "js-sys",
  "rustversion",
  "serde",
- "serde_json",
  "tokio",
  "tracing",
  "url",
  "urlencoding",
- "wasm-bindgen",
  "wasm-bindgen-test",
- "web-sys",
 ]
 
 [[package]]
@@ -3880,10 +3885,12 @@ dependencies = [
  "ciborium",
  "console_error_panic_hook",
  "dioxus",
+ "dioxus-cli-config",
  "dioxus-core",
  "dioxus-core-types",
  "dioxus-devtools",
  "dioxus-document",
+ "dioxus-history",
  "dioxus-html",
  "dioxus-interpreter-js",
  "dioxus-signals",

+ 11 - 10
Cargo.toml

@@ -38,6 +38,7 @@ members = [
     "packages/extension",
     "packages/fullstack",
     "packages/generational-box",
+    "packages/history",
     "packages/hooks",
     "packages/html-internal-macro",
     "packages/html",
@@ -116,15 +117,17 @@ dioxus-core = { path = "packages/core", version = "0.6.0-alpha.3" }
 dioxus-core-types = { path = "packages/core-types", version = "0.6.0-alpha.3" }
 dioxus-core-macro = { path = "packages/core-macro", version = "0.6.0-alpha.3" }
 dioxus-config-macro = { path = "packages/config-macro", version = "0.6.0-alpha.3" }
-dioxus-document = { path = "packages/document", version = "0.6.0-alpha.3" }
 dioxus-router = { path = "packages/router", version = "0.6.0-alpha.3" }
 dioxus-router-macro = { path = "packages/router-macro", version = "0.6.0-alpha.3" }
+dioxus-document = { path = "packages/document", version = "0.6.0-alpha.3", default-features = false }
+dioxus-history = { path = "packages/history", version = "0.6.0-alpha.3", default-features = false }
 dioxus-html = { path = "packages/html", version = "0.6.0-alpha.3", default-features = false }
 dioxus-html-internal-macro = { path = "packages/html-internal-macro", version = "0.6.0-alpha.3" }
 dioxus-hooks = { path = "packages/hooks", version = "0.6.0-alpha.3" }
 dioxus-web = { path = "packages/web", version = "0.6.0-alpha.3", default-features = false }
+dioxus-isrg = { path = "packages/isrg", version = "0.6.0-alpha.3" }
 dioxus-ssr = { path = "packages/ssr", version = "0.6.0-alpha.3", default-features = false }
-dioxus-desktop = { path = "packages/desktop", version = "0.6.0-alpha.3" }
+dioxus-desktop = { path = "packages/desktop", version = "0.6.0-alpha.3", default-features = false }
 dioxus-mobile = { path = "packages/mobile", version = "0.6.0-alpha.3" }
 dioxus-interpreter-js = { path = "packages/interpreter", version = "0.6.0-alpha.3" }
 dioxus-liveview = { path = "packages/liveview", version = "0.6.0-alpha.3" }
@@ -134,19 +137,17 @@ dioxus-rsx = { path = "packages/rsx", version = "0.6.0-alpha.3" }
 dioxus-rsx-hotreload = { path = "packages/rsx-hotreload", version = "0.6.0-alpha.3" }
 dioxus-rsx-rosetta = { path = "packages/rsx-rosetta", version = "0.6.0-alpha.3" }
 dioxus-signals = { path = "packages/signals", version = "0.6.0-alpha.3" }
+dioxus-cli-config = { path = "packages/cli-config", version = "0.6.0-alpha.3" }
+generational-box = { path = "packages/generational-box", version = "0.6.0-alpha.3" }
 dioxus-devtools = { path = "packages/devtools", version = "0.6.0-alpha.3" }
 dioxus-devtools-types = { path = "packages/devtools-types", version = "0.6.0-alpha.3" }
-dioxus-fullstack = { path = "packages/fullstack", version = "0.6.0-alpha.3", default-features = false }
+dioxus-fullstack = { path = "packages/fullstack", version = "0.6.0-alpha.1" }
 dioxus-static-site-generation = { path = "packages/static-generation", version = "0.6.0-alpha.3" }
 dioxus_server_macro = { path = "packages/server-macro", version = "0.6.0-alpha.3", default-features = false }
-dioxus-isrg = { path = "packages/isrg", version = "0.6.0-alpha.3" }
 lazy-js-bundle = { path = "packages/lazy-js-bundle", version = "0.6.0-alpha.3" }
-dioxus-cli-config = { path = "packages/cli-config", version = "0.6.0-alpha.3" }
-generational-box = { path = "packages/generational-box", version = "0.6.0-alpha.3" }
-
-manganis  = { path = "packages/manganis/manganis", version = "0.6.0-alpha.3" }
-manganis-macro  = { path = "packages/manganis/manganis-macro", version = "0.6.0-alpha.3" }
-manganis-core  = { path = "packages/manganis/manganis-core", version = "0.6.0-alpha.3" }
+manganis = { path = "packages/manganis/manganis", version = "0.6.0-alpha.3" }
+manganis-core = { path = "packages/manganis/manganis-core", version = "0.6.0-alpha.3" }
+manganis-macro = { path = "packages/manganis/manganis-macro", version = "0.6.0-alpha.3" }
 
 warnings = { version = "0.2.0" }
 

+ 20 - 5
examples/meta.rs

@@ -11,10 +11,25 @@ fn app() -> Element {
         // You can use the Meta component to render a meta tag into the head of the page
         // Meta tags are useful to provide information about the page to search engines and social media sites
         // This example sets up meta tags for the open graph protocol for social media previews
-        document::Meta { property: "og:title", content: "My Site" }
-        document::Meta { property: "og:type", content: "website" }
-        document::Meta { property: "og:url", content: "https://www.example.com" }
-        document::Meta { property: "og:image", content: "https://example.com/image.jpg" }
-        document::Meta { name: "description", content: "My Site is a site" }
+        document::Meta {
+            property: "og:title",
+            content: "My Site",
+        }
+        document::Meta {
+            property: "og:type",
+            content: "website",
+        }
+        document::Meta {
+            property: "og:url",
+            content: "https://www.example.com",
+        }
+        document::Meta {
+            property: "og:image",
+            content: "https://example.com/image.jpg",
+        }
+        document::Meta {
+            name: "description",
+            content: "My Site is a site",
+        }
     }
 }

+ 1 - 0
packages/desktop/Cargo.toml

@@ -54,6 +54,7 @@ urlencoding = "2.1.2"
 async-trait = "0.1.68"
 tao = { workspace = true, features = ["rwh_05"] }
 once_cell = { workspace = true }
+dioxus-history.workspace = true
 
 
 [target.'cfg(unix)'.dependencies]

+ 3 - 0
packages/desktop/src/webview.rs

@@ -14,6 +14,7 @@ use crate::{
 };
 use dioxus_core::{Runtime, ScopeId, VirtualDom};
 use dioxus_document::Document;
+use dioxus_history::{History, MemoryHistory};
 use dioxus_hooks::to_owned;
 use dioxus_html::{HasFileData, HtmlEvent, PlatformEventData};
 use futures_util::{pin_mut, FutureExt};
@@ -392,9 +393,11 @@ impl WebviewInstance {
         // Provide the desktop context to the virtual dom and edit handler
         edits.set_desktop_context(desktop_context.clone());
         let provider: Rc<dyn Document> = Rc::new(DesktopDocument::new(desktop_context.clone()));
+        let history_provider: Rc<dyn History> = Rc::new(MemoryHistory::default());
         dom.in_runtime(|| {
             ScopeId::ROOT.provide_context(desktop_context.clone());
             ScopeId::ROOT.provide_context(provider);
+            ScopeId::ROOT.provide_context(history_provider);
         });
 
         WebviewInstance {

+ 2 - 1
packages/dioxus-lib/Cargo.toml

@@ -14,6 +14,7 @@ rust-version = "1.79.0"
 dioxus-core = { workspace = true }
 dioxus-html = { workspace = true, optional = true }
 dioxus-document = { workspace = true, optional = true }
+dioxus-history = { workspace = true, optional = true }
 dioxus-core-macro = { workspace = true, optional = true }
 dioxus-config-macro = { workspace = true, optional = true }
 dioxus-hooks = { workspace = true, optional = true }
@@ -27,7 +28,7 @@ dioxus = { workspace = true }
 default = ["macro", "html", "signals", "hooks"]
 signals = ["dep:dioxus-signals"]
 macro = ["dep:dioxus-core-macro", "dep:dioxus-rsx", "dep:dioxus-config-macro"]
-html = ["dep:dioxus-html", "dep:dioxus-document"]
+html = ["dep:dioxus-html", "dep:dioxus-document", "dep:dioxus-history"]
 hooks = ["dep:dioxus-hooks"]
 
 [package.metadata.docs.rs]

+ 7 - 0
packages/dioxus-lib/src/lib.rs

@@ -19,6 +19,9 @@ pub use dioxus_html as html;
 #[cfg(feature = "html")]
 pub use dioxus_document as document;
 
+#[cfg(feature = "html")]
+pub use dioxus_history as history;
+
 #[cfg(feature = "macro")]
 pub use dioxus_rsx as rsx;
 
@@ -26,6 +29,10 @@ pub use dioxus_rsx as rsx;
 pub use dioxus_core_macro as core_macro;
 
 pub mod prelude {
+    #[cfg(feature = "html")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "html")))]
+    pub use dioxus_history::{history, History};
+
     #[cfg(feature = "hooks")]
     pub use crate::hooks::*;
 

+ 6 - 5
packages/dioxus/Cargo.toml

@@ -14,6 +14,7 @@ rust-version = "1.79.0"
 dioxus-core = { workspace = true }
 dioxus-html = { workspace = true, default-features = false, optional = true }
 dioxus-document = { workspace = true, optional = true }
+dioxus-history = { workspace = true, optional = true }
 dioxus-core-macro = { workspace = true, optional = true }
 dioxus-config-macro = { workspace = true, optional = true }
 dioxus-hooks = { workspace = true, optional = true }
@@ -45,18 +46,18 @@ devtools = ["dep:dioxus-devtools", "dioxus-web?/devtools", "dioxus-fullstack?/de
 mounted = ["dioxus-web?/mounted", "dioxus-html?/mounted"]
 file_engine = ["dioxus-web?/file_engine"]
 asset = ["dep:manganis"]
-document = ["dioxus-web?/document", "dioxus-document"]
+document = ["dioxus-web?/document", "dep:dioxus-document", "dep:dioxus-history"]
 
 launch = ["dep:dioxus-config-macro"]
 router = ["dep:dioxus-router"]
 
 # Platforms
-fullstack = ["dep:dioxus-fullstack", "dioxus-config-macro/fullstack", "dep:serde", "dioxus-router?/fullstack"]
+fullstack = ["dep:dioxus-fullstack", "dioxus-config-macro/fullstack", "dep:serde"]
 desktop = ["dep:dioxus-desktop", "dioxus-fullstack?/desktop", "dioxus-config-macro/desktop"]
 mobile = ["dep:dioxus-mobile", "dioxus-fullstack?/mobile", "dioxus-config-macro/mobile"]
-web = ["dep:dioxus-web", "dioxus-fullstack?/web", "dioxus-static-site-generation?/web", "dioxus-config-macro/web", "dioxus-router?/web"]
-ssr = ["dep:dioxus-ssr", "dioxus-router?/ssr", "dioxus-config-macro/ssr"]
-liveview = ["dep:dioxus-liveview", "dioxus-config-macro/liveview", "dioxus-router?/liveview"]
+web = ["dep:dioxus-web", "dioxus-fullstack?/web", "dioxus-static-site-generation?/web", "dioxus-config-macro/web"]
+ssr = ["dep:dioxus-ssr", "dioxus-config-macro/ssr"]
+liveview = ["dep:dioxus-liveview", "dioxus-config-macro/liveview"]
 static-generation = ["dep:dioxus-static-site-generation", "dioxus-config-macro/static-generation"]
 axum = ["server"]
 server = ["dioxus-fullstack?/axum", "dioxus-fullstack?/server", "dioxus-static-site-generation?/server", "ssr", "dioxus-liveview?/axum", "dep:axum"]

+ 8 - 0
packages/dioxus/src/lib.rs

@@ -54,6 +54,10 @@ pub mod events {
 #[cfg_attr(docsrs, doc(cfg(feature = "document")))]
 pub use dioxus_document as document;
 
+#[cfg(feature = "document")]
+#[cfg_attr(docsrs, doc(cfg(feature = "document")))]
+pub use dioxus_history as history;
+
 #[cfg(feature = "html")]
 #[cfg_attr(docsrs, doc(cfg(feature = "html")))]
 pub use dioxus_html as html;
@@ -67,6 +71,10 @@ pub mod prelude {
     #[cfg_attr(docsrs, doc(cfg(feature = "document")))]
     pub use dioxus_document as document;
 
+    #[cfg(feature = "document")]
+    #[cfg_attr(docsrs, doc(cfg(feature = "document")))]
+    pub use dioxus_history::{history, History};
+
     #[cfg(feature = "launch")]
     #[cfg_attr(docsrs, doc(cfg(feature = "launch")))]
     pub use crate::launch::*;

+ 19 - 24
packages/document/src/document.rs

@@ -126,31 +126,26 @@ pub struct NoOpDocument;
 impl Document for NoOpDocument {
     fn eval(&self, _: String) -> Eval {
         let owner = generational_box::Owner::default();
-        let boxed = owner.insert(Box::new(NoOpEvaluator {}) as Box<dyn Evaluator + 'static>);
-        Eval::new(boxed)
-    }
-}
-
-/// An evaluator that does nothing
-#[derive(Default)]
-pub struct NoOpEvaluator;
-
-impl Evaluator for NoOpEvaluator {
-    fn send(&self, _data: serde_json::Value) -> Result<(), EvalError> {
-        Err(EvalError::Unsupported)
-    }
+        struct NoOpEvaluator;
+        impl Evaluator for NoOpEvaluator {
+            fn poll_join(
+                &mut self,
+                _: &mut std::task::Context<'_>,
+            ) -> std::task::Poll<Result<serde_json::Value, EvalError>> {
+                std::task::Poll::Ready(Err(EvalError::Unsupported))
+            }
 
-    fn poll_recv(
-        &mut self,
-        _context: &mut std::task::Context<'_>,
-    ) -> std::task::Poll<Result<serde_json::Value, EvalError>> {
-        std::task::Poll::Ready(Err(EvalError::Unsupported))
-    }
+            fn poll_recv(
+                &mut self,
+                _: &mut std::task::Context<'_>,
+            ) -> std::task::Poll<Result<serde_json::Value, EvalError>> {
+                std::task::Poll::Ready(Err(EvalError::Unsupported))
+            }
 
-    fn poll_join(
-        &mut self,
-        _context: &mut std::task::Context<'_>,
-    ) -> std::task::Poll<Result<serde_json::Value, EvalError>> {
-        std::task::Poll::Ready(Err(EvalError::Unsupported))
+            fn send(&self, _data: serde_json::Value) -> Result<(), EvalError> {
+                Err(EvalError::Unsupported)
+            }
+        }
+        Eval::new(owner.insert(Box::new(NoOpEvaluator)))
     }
 }

+ 11 - 0
packages/history/Cargo.toml

@@ -0,0 +1,11 @@
+[package]
+name = "dioxus-history"
+edition = "2021"
+version.workspace = true
+
+[dependencies]
+dioxus-core = { workspace = true }
+tracing.workspace = true
+
+[dev-dependencies]
+dioxus = { workspace = true, features = ["router"] }

+ 54 - 195
packages/router/src/history/mod.rs → packages/history/src/lib.rs

@@ -1,57 +1,31 @@
-//! History Integration
-//!
-//! dioxus-router-core relies on [`HistoryProvider`]s to store the current Route, and possibly a
-//! history (i.e. a browsers back button) and future (i.e. a browsers forward button).
-//!
-//! To integrate dioxus-router with a any type of history, all you have to do is implement the
-//! [`HistoryProvider`] trait.
-//!
-//! dioxus-router contains two built in history providers:
-//! 1) [`MemoryHistory`] for desktop/mobile/ssr platforms
-//! 2) [`WebHistory`] for web platforms
-
-use std::{any::Any, rc::Rc, sync::Arc};
+use dioxus_core::prelude::{provide_context, provide_root_context};
+use std::{rc::Rc, sync::Arc};
 
 mod memory;
 pub use memory::*;
 
-#[cfg(feature = "web")]
-mod web;
-#[cfg(feature = "web")]
-pub use web::*;
-#[cfg(feature = "web")]
-pub(crate) mod web_history;
-
-#[cfg(feature = "liveview")]
-mod liveview;
-#[cfg(feature = "liveview")]
-pub use liveview::*;
-
-// #[cfg(feature = "web")]
-// mod web_hash;
-// #[cfg(feature = "web")]
-// pub use web_hash::*;
-
-use crate::routable::Routable;
+/// Get the history provider for the current platform if the platform doesn't implement a history functionality.
+pub fn history() -> Rc<dyn History> {
+    match dioxus_core::prelude::try_consume_context::<Rc<dyn History>>() {
+        Some(history) => history,
+        None => {
+            tracing::error!("Unable to find a history provider in the renderer. Make sure your renderer supports the Router. Falling back to the in-memory history provider.");
+            provide_root_context(Rc::new(MemoryHistory::default()))
+        }
+    }
+}
 
-#[cfg(feature = "web")]
-pub(crate) mod web_scroll;
+/// Provide a history context to the current component.
+pub fn provide_history_context(history: Rc<dyn History>) {
+    provide_context(history);
+}
 
-/// An integration with some kind of navigation history.
-///
-/// Depending on your use case, your implementation may deviate from the described procedure. This
-/// is fine, as long as both `current_route` and `current_query` match the described format.
-///
-/// 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<R: Routable> {
+pub trait History {
     /// Get the path of the current URL.
     ///
     /// **Must start** with `/`. **Must _not_ contain** the prefix.
     ///
     /// ```rust
-    /// # use dioxus_router::prelude::*;
     /// # use dioxus::prelude::*;
     /// # #[component]
     /// # fn Index() -> Element { VNode::empty() }
@@ -64,14 +38,14 @@ pub trait HistoryProvider<R: Routable> {
     ///     #[route("/some-other-page")]
     ///     OtherPage {},
     /// }
-    /// let mut history = MemoryHistory::<Route>::default();
-    /// assert_eq!(history.current_route().to_string(), "/");
+    /// let mut history = dioxus::history::MemoryHistory::default();
+    /// assert_eq!(history.current_route(), "/");
     ///
-    /// history.push(Route::OtherPage {});
-    /// assert_eq!(history.current_route().to_string(), "/some-other-page");
+    /// history.push(Route::OtherPage {}.to_string());
+    /// assert_eq!(history.current_route(), "/some-other-page");
     /// ```
     #[must_use]
-    fn current_route(&self) -> R;
+    fn current_route(&self) -> String;
 
     /// Get the current path prefix of the URL.
     ///
@@ -89,7 +63,6 @@ pub trait HistoryProvider<R: Routable> {
     /// If a [`HistoryProvider`] cannot know this, it should return [`true`].
     ///
     /// ```rust
-    /// # use dioxus_router::prelude::*;
     /// # use dioxus::prelude::*;
     /// # #[component]
     /// # fn Index() -> Element { VNode::empty() }
@@ -101,10 +74,10 @@ pub trait HistoryProvider<R: Routable> {
     ///     #[route("/other")]
     ///     Other {},
     /// }
-    /// let mut history = MemoryHistory::<Route>::default();
+    /// let mut history = dioxus::history::MemoryHistory::default();
     /// assert_eq!(history.can_go_back(), false);
     ///
-    /// history.push(Route::Other {});
+    /// history.push(Route::Other {}.to_string());
     /// assert_eq!(history.can_go_back(), true);
     /// ```
     #[must_use]
@@ -118,7 +91,6 @@ pub trait HistoryProvider<R: Routable> {
     /// might be called, even if `can_go_back` returns [`false`].
     ///
     /// ```rust
-    /// # use dioxus_router::prelude::*;
     /// # use dioxus::prelude::*;
     /// # #[component]
     /// # fn Index() -> Element { VNode::empty() }
@@ -131,26 +103,25 @@ pub trait HistoryProvider<R: Routable> {
     ///     #[route("/some-other-page")]
     ///     OtherPage {},
     /// }
-    /// let mut history = MemoryHistory::<Route>::default();
-    /// assert_eq!(history.current_route().to_string(), "/");
+    /// let mut history = dioxus::history::MemoryHistory::default();
+    /// assert_eq!(history.current_route(), "/");
     ///
     /// history.go_back();
-    /// assert_eq!(history.current_route().to_string(), "/");
+    /// assert_eq!(history.current_route(), "/");
     ///
-    /// history.push(Route::OtherPage {});
-    /// assert_eq!(history.current_route().to_string(), "/some-other-page");
+    /// history.push(Route::OtherPage {}.to_string());
+    /// assert_eq!(history.current_route(), "/some-other-page");
     ///
     /// history.go_back();
-    /// assert_eq!(history.current_route().to_string(), "/");
+    /// assert_eq!(history.current_route(), "/");
     /// ```
-    fn go_back(&mut self);
+    fn go_back(&self);
 
     /// Check whether there is a future page to navigate forward to.
     ///
     /// If a [`HistoryProvider`] cannot know this, it should return [`true`].
     ///
     /// ```rust
-    /// # use dioxus_router::prelude::*;
     /// # use dioxus::prelude::*;
     /// # #[component]
     /// # fn Index() -> Element { VNode::empty() }
@@ -163,10 +134,10 @@ pub trait HistoryProvider<R: Routable> {
     ///     #[route("/some-other-page")]
     ///     OtherPage {},
     /// }
-    /// let mut history = MemoryHistory::<Route>::default();
+    /// let mut history = dioxus::history::MemoryHistory::default();
     /// assert_eq!(history.can_go_forward(), false);
     ///
-    /// history.push(Route::OtherPage {});
+    /// history.push(Route::OtherPage {}.to_string());
     /// assert_eq!(history.can_go_forward(), false);
     ///
     /// history.go_back();
@@ -183,7 +154,6 @@ pub trait HistoryProvider<R: Routable> {
     /// might be called, even if `can_go_forward` returns [`false`].
     ///
     /// ```rust
-    /// # use dioxus_router::prelude::*;
     /// # use dioxus::prelude::*;
     /// # #[component]
     /// # fn Index() -> Element { VNode::empty() }
@@ -196,17 +166,17 @@ pub trait HistoryProvider<R: Routable> {
     ///     #[route("/some-other-page")]
     ///     OtherPage {},
     /// }
-    /// let mut history = MemoryHistory::<Route>::default();
-    /// history.push(Route::OtherPage {});
-    /// assert_eq!(history.current_route(), Route::OtherPage {});
+    /// let mut history = dioxus::history::MemoryHistory::default();
+    /// history.push(Route::OtherPage {}.to_string());
+    /// assert_eq!(history.current_route(), Route::OtherPage {}.to_string());
     ///
     /// history.go_back();
-    /// assert_eq!(history.current_route(), Route::Index {});
+    /// assert_eq!(history.current_route(), Route::Index {}.to_string());
     ///
     /// history.go_forward();
-    /// assert_eq!(history.current_route(), Route::OtherPage {});
+    /// assert_eq!(history.current_route(), Route::OtherPage {}.to_string());
     /// ```
-    fn go_forward(&mut self);
+    fn go_forward(&self);
 
     /// Go to another page.
     ///
@@ -216,7 +186,6 @@ pub trait HistoryProvider<R: Routable> {
     /// 3. Clear the navigation future.
     ///
     /// ```rust
-    /// # use dioxus_router::prelude::*;
     /// # use dioxus::prelude::*;
     /// # #[component]
     /// # fn Index() -> Element { VNode::empty() }
@@ -229,14 +198,14 @@ pub trait HistoryProvider<R: Routable> {
     ///     #[route("/some-other-page")]
     ///     OtherPage {},
     /// }
-    /// let mut history = MemoryHistory::<Route>::default();
-    /// assert_eq!(history.current_route(), Route::Index {});
+    /// let mut history = dioxus::history::MemoryHistory::default();
+    /// assert_eq!(history.current_route(), Route::Index {}.to_string());
     ///
-    /// history.push(Route::OtherPage {});
-    /// assert_eq!(history.current_route(), Route::OtherPage {});
+    /// history.push(Route::OtherPage {}.to_string());
+    /// assert_eq!(history.current_route(), Route::OtherPage {}.to_string());
     /// assert!(history.can_go_back());
     /// ```
-    fn push(&mut self, route: R);
+    fn push(&self, route: String);
 
     /// Replace the current page with another one.
     ///
@@ -245,7 +214,6 @@ pub trait HistoryProvider<R: Routable> {
     /// untouched.
     ///
     /// ```rust
-    /// # use dioxus_router::prelude::*;
     /// # use dioxus::prelude::*;
     /// # #[component]
     /// # fn Index() -> Element { VNode::empty() }
@@ -258,14 +226,14 @@ pub trait HistoryProvider<R: Routable> {
     ///     #[route("/some-other-page")]
     ///     OtherPage {},
     /// }
-    /// let mut history = MemoryHistory::<Route>::default();
-    /// assert_eq!(history.current_route(), Route::Index {});
+    /// let mut history = dioxus::history::MemoryHistory::default();
+    /// assert_eq!(history.current_route(), Route::Index {}.to_string());
     ///
-    /// history.replace(Route::OtherPage {});
-    /// assert_eq!(history.current_route(), Route::OtherPage {});
+    /// history.replace(Route::OtherPage {}.to_string());
+    /// assert_eq!(history.current_route(), Route::OtherPage {}.to_string());
     /// assert!(!history.can_go_back());
     /// ```
-    fn replace(&mut self, path: R);
+    fn replace(&self, path: String);
 
     /// Navigate to an external URL.
     ///
@@ -274,7 +242,7 @@ pub trait HistoryProvider<R: Routable> {
     ///
     /// Returning [`false`] will cause the router to handle the external navigation failure.
     #[allow(unused_variables)]
-    fn external(&mut self, url: String) -> bool {
+    fn external(&self, url: String) -> bool {
         false
     }
 
@@ -283,120 +251,11 @@ pub trait HistoryProvider<R: Routable> {
     /// Some [`HistoryProvider`]s may receive URL updates from outside the router. When such
     /// updates are received, they should call `callback`, which will cause the router to update.
     #[allow(unused_variables)]
-    fn updater(&mut self, callback: Arc<dyn Fn() + Send + Sync>) {}
-}
-
-pub(crate) trait AnyHistoryProvider {
-    fn parse_route(&self, route: &str) -> Result<Rc<dyn Any>, String>;
-
-    #[must_use]
-    fn current_route(&self) -> Rc<dyn Any>;
-
-    #[must_use]
-    fn can_go_back(&self) -> bool {
-        true
-    }
+    fn updater(&self, callback: Arc<dyn Fn() + Send + Sync>) {}
 
-    fn go_back(&mut self);
-
-    #[must_use]
-    fn can_go_forward(&self) -> bool {
-        true
-    }
-
-    fn go_forward(&mut self);
-
-    fn push(&mut self, route: Rc<dyn Any>);
-
-    fn replace(&mut self, path: Rc<dyn Any>);
-
-    #[allow(unused_variables)]
-    fn external(&mut self, url: String) -> bool {
+    /// Whether the router should include the legacy prevent default attribute instead of the new
+    /// prevent default method. This should only be used by liveview.
+    fn include_prevent_default(&self) -> bool {
         false
     }
-
-    #[allow(unused_variables)]
-    fn updater(&mut self, callback: Arc<dyn Fn() + Send + Sync>) {}
-
-    #[cfg(feature = "liveview")]
-    fn is_liveview(&self) -> bool;
-}
-
-pub(crate) struct AnyHistoryProviderImplWrapper<R, H> {
-    inner: H,
-    _marker: std::marker::PhantomData<R>,
-}
-
-impl<R, H> AnyHistoryProviderImplWrapper<R, H> {
-    pub fn new(inner: H) -> Self {
-        Self {
-            inner,
-            _marker: std::marker::PhantomData,
-        }
-    }
-}
-
-impl<R, H: Default> Default for AnyHistoryProviderImplWrapper<R, H> {
-    fn default() -> Self {
-        Self::new(H::default())
-    }
-}
-
-impl<R, H: 'static> AnyHistoryProvider for AnyHistoryProviderImplWrapper<R, H>
-where
-    R: Routable,
-    <R as std::str::FromStr>::Err: std::fmt::Display,
-    H: HistoryProvider<R>,
-{
-    fn parse_route(&self, route: &str) -> Result<Rc<dyn Any>, String> {
-        R::from_str(route)
-            .map_err(|err| err.to_string())
-            .map(|route| Rc::new(route) as Rc<dyn Any>)
-    }
-
-    fn current_route(&self) -> Rc<dyn Any> {
-        let route = self.inner.current_route();
-        Rc::new(route)
-    }
-
-    fn can_go_back(&self) -> bool {
-        self.inner.can_go_back()
-    }
-
-    fn go_back(&mut self) {
-        self.inner.go_back()
-    }
-
-    fn can_go_forward(&self) -> bool {
-        self.inner.can_go_forward()
-    }
-
-    fn go_forward(&mut self) {
-        self.inner.go_forward()
-    }
-
-    fn push(&mut self, route: Rc<dyn Any>) {
-        self.inner
-            .push(route.downcast::<R>().unwrap().as_ref().clone())
-    }
-
-    fn replace(&mut self, route: Rc<dyn Any>) {
-        self.inner
-            .replace(route.downcast::<R>().unwrap().as_ref().clone())
-    }
-
-    fn external(&mut self, url: String) -> bool {
-        self.inner.external(url)
-    }
-
-    fn updater(&mut self, callback: Arc<dyn Fn() + Send + Sync>) {
-        self.inner.updater(callback)
-    }
-
-    #[cfg(feature = "liveview")]
-    fn is_liveview(&self) -> bool {
-        use std::any::TypeId;
-
-        TypeId::of::<H>() == TypeId::of::<LiveviewHistory<R>>()
-    }
 }

+ 100 - 0
packages/history/src/memory.rs

@@ -0,0 +1,100 @@
+use std::cell::RefCell;
+
+use crate::History;
+
+struct MemoryHistoryState {
+    current: String,
+    history: Vec<String>,
+    future: Vec<String>,
+}
+
+/// A [`History`] provider that stores all navigation information in memory.
+pub struct MemoryHistory {
+    state: RefCell<MemoryHistoryState>,
+}
+
+impl Default for MemoryHistory {
+    fn default() -> Self {
+        Self::with_initial_path("/")
+    }
+}
+
+impl MemoryHistory {
+    /// Create a [`MemoryHistory`] starting at `path`.
+    ///
+    /// ```rust
+    /// # use dioxus_router::prelude::*;
+    /// # use dioxus::prelude::*;
+    /// # #[component]
+    /// # fn Index() -> Element { VNode::empty() }
+    /// # #[component]
+    /// # fn OtherPage() -> Element { VNode::empty() }
+    /// #[derive(Clone, Routable, Debug, PartialEq)]
+    /// enum Route {
+    ///     #[route("/")]
+    ///     Index {},
+    ///     #[route("/some-other-page")]
+    ///     OtherPage {},
+    /// }
+    ///
+    /// let mut history = dioxus_history::MemoryHistory::with_initial_path(Route::Index {});
+    /// assert_eq!(history.current_route(), Route::Index {}.to_string());
+    /// assert_eq!(history.can_go_back(), false);
+    /// ```
+    pub fn with_initial_path(path: impl ToString) -> Self {
+        Self {
+            state: MemoryHistoryState{
+            current: path.to_string().parse().unwrap_or_else(|err| {
+                panic!("index route does not exist:\n{err}\n use MemoryHistory::with_initial_path to set a custom path")
+            }),
+            history: Vec::new(),
+            future: Vec::new(),}.into()
+        }
+    }
+}
+
+impl History for MemoryHistory {
+    fn current_route(&self) -> String {
+        self.state.borrow().current.clone()
+    }
+
+    fn can_go_back(&self) -> bool {
+        !self.state.borrow().history.is_empty()
+    }
+
+    fn go_back(&self) {
+        let mut write = self.state.borrow_mut();
+        if let Some(last) = write.history.pop() {
+            let old = std::mem::replace(&mut write.current, last);
+            write.future.push(old);
+        }
+    }
+
+    fn can_go_forward(&self) -> bool {
+        !self.state.borrow().future.is_empty()
+    }
+
+    fn go_forward(&self) {
+        let mut write = self.state.borrow_mut();
+        if let Some(next) = write.future.pop() {
+            let old = std::mem::replace(&mut write.current, next);
+            write.history.push(old);
+        }
+    }
+
+    fn push(&self, new: String) {
+        let mut write = self.state.borrow_mut();
+        // don't push the same route twice
+        if write.current == new {
+            return;
+        }
+        let old = std::mem::replace(&mut write.current, new);
+        write.history.push(old);
+        write.future.clear();
+    }
+
+    fn replace(&self, path: String) {
+        let mut write = self.state.borrow_mut();
+        write.current = path;
+    }
+}

+ 1 - 0
packages/liveview/Cargo.toml

@@ -24,6 +24,7 @@ serde = { version = "1.0.151", features = ["derive"] }
 serde_json = "1.0.91"
 dioxus-html = { workspace = true, features = ["serialize", "mounted"] }
 dioxus-document = { workspace = true }
+dioxus-history = { workspace = true }
 rustc-hash = { workspace = true }
 dioxus-core = { workspace = true, features = ["serialize"] }
 dioxus-interpreter-js = { workspace = true, features = ["binary-protocol"] }

+ 27 - 18
packages/liveview/src/eval.rs → packages/liveview/src/document.rs

@@ -1,28 +1,12 @@
 use dioxus_core::ScopeId;
 use dioxus_document::{Document, Eval, EvalError, Evaluator};
+use dioxus_history::History;
 use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage};
 use std::rc::Rc;
 
+use crate::history::LiveviewHistory;
 use crate::query::{Query, QueryEngine};
 
-/// Provides the LiveviewDocument through [`ScopeId::provide_context`].
-pub fn init_eval() {
-    let query = ScopeId::ROOT.consume_context::<QueryEngine>().unwrap();
-    let provider: Rc<dyn Document> = Rc::new(LiveviewDocument { query });
-    ScopeId::ROOT.provide_context(provider);
-}
-
-/// Reprints the liveview-target's provider of evaluators.
-pub struct LiveviewDocument {
-    query: QueryEngine,
-}
-
-impl Document for LiveviewDocument {
-    fn eval(&self, js: String) -> Eval {
-        Eval::new(LiveviewEvaluator::create(self.query.clone(), js))
-    }
-}
-
 /// Represents a liveview-target's JavaScript evaluator.
 pub(crate) struct LiveviewEvaluator {
     query: Query<serde_json::Value>,
@@ -75,3 +59,28 @@ impl Evaluator for LiveviewEvaluator {
             .map_err(|e| EvalError::Communication(e.to_string()))
     }
 }
+
+/// Provides the LiveviewDocument through [`ScopeId::provide_context`].
+pub fn init_document() {
+    let query = ScopeId::ROOT.consume_context::<QueryEngine>().unwrap();
+    let provider: Rc<dyn Document> = Rc::new(LiveviewDocument {
+        query: query.clone(),
+    });
+    ScopeId::ROOT.provide_context(provider);
+    let history = LiveviewHistory::new(Rc::new(move |script: &str| {
+        Eval::new(LiveviewEvaluator::create(query.clone(), script.to_string()))
+    }));
+    let history: Rc<dyn History> = Rc::new(history);
+    ScopeId::ROOT.provide_context(history);
+}
+
+/// Reprints the liveview-target's provider of evaluators.
+pub struct LiveviewDocument {
+    query: QueryEngine,
+}
+
+impl Document for LiveviewDocument {
+    fn eval(&self, js: String) -> Eval {
+        Eval::new(LiveviewEvaluator::create(self.query.clone(), js))
+    }
+}

+ 51 - 104
packages/router/src/history/liveview.rs → packages/liveview/src/history.rs

@@ -1,27 +1,21 @@
-use super::HistoryProvider;
-use crate::routable::Routable;
-use dioxus_lib::document::Eval;
-use dioxus_lib::prelude::*;
+use dioxus_core::prelude::spawn;
+use dioxus_document::Eval;
+use dioxus_history::History;
 use serde::{Deserialize, Serialize};
+use std::rc::Rc;
 use std::sync::{Mutex, RwLock};
-use std::{collections::BTreeMap, rc::Rc, str::FromStr, sync::Arc};
+use std::{collections::BTreeMap, sync::Arc};
 
 /// A [`HistoryProvider`] that evaluates history through JS.
-pub struct LiveviewHistory<R: Routable>
-where
-    <R as FromStr>::Err: std::fmt::Display,
-{
-    action_tx: tokio::sync::mpsc::UnboundedSender<Action<R>>,
-    timeline: Arc<Mutex<Timeline<R>>>,
+pub(crate) struct LiveviewHistory {
+    action_tx: tokio::sync::mpsc::UnboundedSender<Action>,
+    timeline: Arc<Mutex<Timeline>>,
     updater_callback: Arc<RwLock<Arc<dyn Fn() + Send + Sync>>>,
 }
 
-struct Timeline<R: Routable>
-where
-    <R as FromStr>::Err: std::fmt::Display,
-{
+struct Timeline {
     current_index: usize,
-    routes: BTreeMap<usize, R>,
+    routes: BTreeMap<usize, String>,
 }
 
 #[derive(Serialize, Deserialize, Debug)]
@@ -30,28 +24,22 @@ struct State {
 }
 
 #[derive(Serialize, Deserialize, Debug)]
-struct Session<R: Routable>
-where
-    <R as FromStr>::Err: std::fmt::Display,
-{
+struct Session {
     #[serde(with = "routes")]
-    routes: BTreeMap<usize, R>,
+    routes: BTreeMap<usize, String>,
     last_visited: usize,
 }
 
-enum Action<R: Routable> {
+enum Action {
     GoBack,
     GoForward,
-    Push(R),
-    Replace(R),
+    Push(String),
+    Replace(String),
     External(String),
 }
 
-impl<R: Routable> Timeline<R>
-where
-    <R as FromStr>::Err: std::fmt::Display,
-{
-    fn new(initial_path: R) -> Self {
+impl Timeline {
+    fn new(initial_path: String) -> Self {
         Self {
             current_index: 0,
             routes: BTreeMap::from([(0, initial_path)]),
@@ -60,9 +48,9 @@ where
 
     fn init(
         &mut self,
-        route: R,
+        route: String,
         state: Option<State>,
-        session: Option<Session<R>>,
+        session: Option<Session>,
         depth: usize,
     ) -> State {
         if let Some(session) = session {
@@ -88,7 +76,7 @@ where
         state
     }
 
-    fn update(&mut self, route: R, state: Option<State>) -> State {
+    fn update(&mut self, route: String, state: Option<State>) -> State {
         if let Some(state) = state {
             self.current_index = state.index;
             self.routes.insert(self.current_index, route);
@@ -98,7 +86,7 @@ where
         }
     }
 
-    fn push(&mut self, route: R) -> State {
+    fn push(&mut self, route: String) -> State {
         // top of stack
         let index = self.current_index + 1;
         self.current_index = index;
@@ -109,18 +97,18 @@ where
         }
     }
 
-    fn replace(&mut self, route: R) -> State {
+    fn replace(&mut self, route: String) -> State {
         self.routes.insert(self.current_index, route);
         State {
             index: self.current_index,
         }
     }
 
-    fn current_route(&self) -> &R {
+    fn current_route(&self) -> &str {
         &self.routes[&self.current_index]
     }
 
-    fn session(&self) -> Session<R> {
+    fn session(&self) -> Session {
         Session {
             routes: self.routes.clone(),
             last_visited: self.current_index,
@@ -128,30 +116,19 @@ where
     }
 }
 
-impl<R: Routable> Default for LiveviewHistory<R>
-where
-    <R as FromStr>::Err: std::fmt::Display,
-{
-    fn default() -> Self {
-        Self::new()
-    }
-}
-
-impl<R: Routable> LiveviewHistory<R>
-where
-    <R as FromStr>::Err: std::fmt::Display,
-{
+impl LiveviewHistory {
     /// Create a [`LiveviewHistory`] in the given scope.
     /// When using a [`LiveviewHistory`] in combination with use_eval, history must be untampered with.
     ///
     /// # Panics
     ///
     /// Panics if the function is not called in a dioxus runtime with a Liveview context.
-    pub fn new() -> Self {
+    pub(crate) fn new(eval: Rc<dyn Fn(&str) -> Eval>) -> Self {
         Self::new_with_initial_path(
             "/".parse().unwrap_or_else(|err| {
                 panic!("index route does not exist:\n{}\n use LiveviewHistory::new_with_initial_path to set a custom path", err)
             }),
+            eval
         )
     }
 
@@ -161,22 +138,17 @@ where
     /// # Panics
     ///
     /// Panics if the function is not called in a dioxus runtime with a Liveview context.
-    pub fn new_with_initial_path(initial_path: R) -> Self {
-        let (action_tx, mut action_rx) = tokio::sync::mpsc::unbounded_channel::<Action<R>>();
+    fn new_with_initial_path(initial_path: String, eval: Rc<dyn Fn(&str) -> Eval>) -> Self {
+        let (action_tx, mut action_rx) = tokio::sync::mpsc::unbounded_channel::<Action>();
 
         let timeline = Arc::new(Mutex::new(Timeline::new(initial_path)));
         let updater_callback: Arc<RwLock<Arc<dyn Fn() + Send + Sync>>> =
             Arc::new(RwLock::new(Arc::new(|| {})));
 
-        let eval_provider = dioxus_lib::document::document();
-
-        let create_eval = Rc::new(move |script: &str| eval_provider.eval(script.to_string()))
-            as Rc<dyn Fn(&str) -> Eval>;
-
         // Listen to server actions
         spawn({
             let timeline = timeline.clone();
-            let create_eval = create_eval.clone();
+            let create_eval = eval.clone();
             async move {
                 loop {
                     let eval = action_rx.recv().await.expect("sender to exist");
@@ -236,7 +208,7 @@ where
         spawn({
             let updater = updater_callback.clone();
             let timeline = timeline.clone();
-            let create_eval = create_eval.clone();
+            let create_eval = eval.clone();
             async move {
                 let mut popstate_eval = {
                     let init_eval = create_eval(
@@ -249,16 +221,11 @@ where
                         ];
                     "#,
                     ).await.expect("serializable state");
-                    let (route, state, session, depth) = serde_json::from_value::<(
-                        String,
-                        Option<State>,
-                        Option<Session<R>>,
-                        usize,
-                    )>(init_eval)
-                    .expect("serializable state");
-                    let Ok(route) = R::from_str(&route.to_string()) else {
-                        return;
-                    };
+                    let (route, state, session, depth) =
+                        serde_json::from_value::<(String, Option<State>, Option<Session>, usize)>(
+                            init_eval,
+                        )
+                        .expect("serializable state");
                     let mut timeline = timeline.lock().expect("unpoisoned mutex");
                     let state = timeline.init(route.clone(), state, session, depth);
                     let state = serde_json::to_string(&state).expect("serializable state");
@@ -291,9 +258,6 @@ where
                     };
                     let (route, state) = serde_json::from_value::<(String, Option<State>)>(event)
                         .expect("serializable state");
-                    let Ok(route) = R::from_str(&route.to_string()) else {
-                        return;
-                    };
                     let mut timeline = timeline.lock().expect("unpoisoned mutex");
                     let state = timeline.update(route.clone(), state);
                     let state = serde_json::to_string(&state).expect("serializable state");
@@ -322,34 +286,31 @@ where
     }
 }
 
-impl<R: Routable> HistoryProvider<R> for LiveviewHistory<R>
-where
-    <R as FromStr>::Err: std::fmt::Display,
-{
-    fn go_back(&mut self) {
+impl History for LiveviewHistory {
+    fn go_back(&self) {
         let _ = self.action_tx.send(Action::GoBack);
     }
 
-    fn go_forward(&mut self) {
+    fn go_forward(&self) {
         let _ = self.action_tx.send(Action::GoForward);
     }
 
-    fn push(&mut self, route: R) {
+    fn push(&self, route: String) {
         let _ = self.action_tx.send(Action::Push(route));
     }
 
-    fn replace(&mut self, route: R) {
+    fn replace(&self, route: String) {
         let _ = self.action_tx.send(Action::Replace(route));
     }
 
-    fn external(&mut self, url: String) -> bool {
+    fn external(&self, url: String) -> bool {
         let _ = self.action_tx.send(Action::External(url));
         true
     }
 
-    fn current_route(&self) -> R {
+    fn current_route(&self) -> String {
         let timeline = self.timeline.lock().expect("unpoisoned mutex");
-        timeline.current_route().clone()
+        timeline.current_route().to_string()
     }
 
     fn can_go_back(&self) -> bool {
@@ -377,23 +338,20 @@ where
             })
     }
 
-    fn updater(&mut self, callback: Arc<dyn Fn() + Send + Sync>) {
+    fn updater(&self, callback: Arc<dyn Fn() + Send + Sync>) {
         let mut updater_callback = self.updater_callback.write().unwrap();
         *updater_callback = callback;
     }
 }
 
 mod routes {
-    use crate::prelude::Routable;
-    use core::str::FromStr;
     use serde::de::{MapAccess, Visitor};
     use serde::{ser::SerializeMap, Deserializer, Serializer};
     use std::collections::BTreeMap;
 
-    pub fn serialize<S, R>(routes: &BTreeMap<usize, R>, serializer: S) -> Result<S::Ok, S::Error>
+    pub fn serialize<S>(routes: &BTreeMap<usize, String>, serializer: S) -> Result<S::Ok, S::Error>
     where
         S: Serializer,
-        R: Routable,
     {
         let mut map = serializer.serialize_map(Some(routes.len()))?;
         for (index, route) in routes.iter() {
@@ -402,22 +360,14 @@ mod routes {
         map.end()
     }
 
-    pub fn deserialize<'de, D, R>(deserializer: D) -> Result<BTreeMap<usize, R>, D::Error>
+    pub fn deserialize<'de, D>(deserializer: D) -> Result<BTreeMap<usize, String>, D::Error>
     where
         D: Deserializer<'de>,
-        R: Routable,
-        <R as FromStr>::Err: std::fmt::Display,
     {
-        struct BTreeMapVisitor<R> {
-            marker: std::marker::PhantomData<R>,
-        }
+        struct BTreeMapVisitor {}
 
-        impl<'de, R> Visitor<'de> for BTreeMapVisitor<R>
-        where
-            R: Routable,
-            <R as FromStr>::Err: std::fmt::Display,
-        {
-            type Value = BTreeMap<usize, R>;
+        impl<'de> Visitor<'de> for BTreeMapVisitor {
+            type Value = BTreeMap<usize, String>;
 
             fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
                 formatter.write_str("a map with indices and routable values")
@@ -430,15 +380,12 @@ mod routes {
                 let mut routes = BTreeMap::new();
                 while let Some((index, route)) = map.next_entry::<String, String>()? {
                     let index = index.parse::<usize>().map_err(serde::de::Error::custom)?;
-                    let route = R::from_str(&route).map_err(serde::de::Error::custom)?;
                     routes.insert(index, route);
                 }
                 Ok(routes)
             }
         }
 
-        deserializer.deserialize_map(BTreeMapVisitor {
-            marker: std::marker::PhantomData,
-        })
+        deserializer.deserialize_map(BTreeMapVisitor {})
     }
 }

+ 2 - 1
packages/liveview/src/lib.rs

@@ -13,8 +13,9 @@ use dioxus_interpreter_js::NATIVE_JS;
 use futures_util::{SinkExt, StreamExt};
 pub use pool::*;
 mod config;
-mod eval;
+mod document;
 mod events;
+mod history;
 pub use config::*;
 #[cfg(feature = "axum")]
 pub mod launch;

+ 4 - 4
packages/liveview/src/pool.rs

@@ -1,6 +1,6 @@
 use crate::{
+    document::init_document,
     element::LiveviewElement,
-    eval::init_eval,
     events::SerializedHtmlEventConverter,
     query::{QueryEngine, QueryResult},
     LiveViewError,
@@ -130,9 +130,9 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li
     // Create the a proxy for query engine
     let (query_tx, mut query_rx) = tokio::sync::mpsc::unbounded_channel();
     let query_engine = QueryEngine::new(query_tx);
-    vdom.in_runtime(|| {
-        ScopeId::ROOT.provide_context(query_engine.clone());
-        init_eval();
+    vdom.runtime().on_scope(ScopeId::ROOT, || {
+        provide_context(query_engine.clone());
+        init_document();
     });
 
     // pin the futures so we can use select!

+ 0 - 1
packages/liveview/src/query.rs

@@ -43,7 +43,6 @@ let dioxus = {
 }"#;
 
 /// Tracks what query ids are currently active
-
 pub(crate) struct SharedSlab<T = ()> {
     pub(crate) slab: Rc<RefCell<Slab<T>>>,
 }

+ 17 - 1
packages/router-macro/src/route.rs

@@ -242,7 +242,23 @@ impl Route {
                 quote! {
                     #[allow(unused)]
                     (#last_index.., Self::#name { #field_name, .. }) => {
-                        #field_name.render(level - #last_index)
+                        rsx! {
+                            dioxus_router::components::child_router::ChildRouter {
+                                route: #field_name,
+                                // Try to parse the current route as a parent route, and then match it as a child route
+                                parse_route_from_root_route: |__route| if let Ok(__route) = __route.parse() {
+                                    if let Self::#name { #field_name, .. } = __route {
+                                        Some(#field_name)
+                                    } else {
+                                        None
+                                    }
+                                } else {
+                                    None
+                                },
+                                // Try to parse the child route and turn it into a parent route
+                                format_route_as_root_route: |#field_name| Self::#name { #field_name: #field_name }.to_string(),
+                            }
+                        }
                     }
                 }
             }

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

@@ -274,7 +274,7 @@ pub(crate) enum RouteTreeSegmentData<'a> {
     Redirect(&'a Redirect),
 }
 
-impl<'a> RouteTreeSegmentData<'a> {
+impl RouteTreeSegmentData<'_> {
     pub fn to_tokens(
         &self,
         nests: &[Nest],

+ 2 - 21
packages/router/Cargo.toml

@@ -11,24 +11,11 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
 
 [dependencies]
 dioxus-lib = { workspace = true }
+dioxus-history = { workspace = true } 
 dioxus-router-macro = { workspace = true }
-gloo = { version = "0.8.0", optional = true }
 tracing = { workspace = true }
 urlencoding = "2.1.3"
-serde = { version = "1", features = ["derive"], optional = true }
-serde_json = { version = "1.0.91", optional = true }
 url = "2.3.1"
-wasm-bindgen = { workspace = true, optional = true }
-web-sys = { version = "0.3.60", optional = true, features = [
-    "ScrollRestoration",
-] }
-js-sys = { version = "0.3.63", optional = true }
-gloo-utils = { version = "0.1.6", optional = true }
-dioxus-liveview = { workspace = true, optional = true }
-dioxus-ssr = { workspace = true, optional = true }
-http = { workspace = true, optional = true }
-dioxus-fullstack = { workspace = true, optional = true }
-tokio = { workspace = true, features = ["full"], optional = true }
 dioxus-cli-config = { workspace = true }
 rustversion = "1.0.17"
 
@@ -36,18 +23,11 @@ rustversion = "1.0.17"
 # dev-dependncey crates
 [target.'cfg(target_family = "wasm")'.dev-dependencies]
 console_error_panic_hook = "0.1.7"
-dioxus-router = { workspace = true, features = ["web"] }
-# dioxus-web = { workspace = true }
 gloo = "0.8.0"
 wasm-bindgen-test = "0.3.33"
 
 [features]
 default = []
-ssr = []
-liveview = ["dioxus-liveview", "dep:tokio", "dep:serde", "dep:serde_json"]
-wasm_test = []
-web = ["dep:gloo", "dep:web-sys", "dep:wasm-bindgen", "dep:gloo-utils", "dep:js-sys", "dioxus-router-macro/web"]
-fullstack = ["dep:dioxus-fullstack"]
 
 [dev-dependencies]
 axum = { workspace = true, features = ["ws"] }
@@ -57,6 +37,7 @@ criterion = { workspace = true, features = ["async_tokio", "html_reports"] }
 ciborium = { version = "0.2.1" }
 base64 = { version = "0.21.0" }
 serde = { version = "1", features = ["derive"] }
+tokio = { workspace = true, features = ["full"] }
 
 [package.metadata.docs.rs]
 cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]

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

@@ -1,204 +0,0 @@
-use dioxus::prelude::*;
-use std::str::FromStr;
-
-#[cfg(feature = "liveview")]
-#[tokio::main]
-async fn main() {
-    use axum::{extract::ws::WebSocketUpgrade, response::Html, routing::get, Router};
-
-    let listen_address: std::net::SocketAddr = ([127, 0, 0, 1], 3030).into();
-    let view = dioxus_liveview::LiveViewPool::new();
-    let app = Router::new()
-        .fallback(get(move || async move {
-            Html(format!(
-                r#"
-                    <!DOCTYPE html>
-                    <html>
-                        <head></head>
-                        <body><div id="main"></div></body>
-                        {glue}
-                    </html>
-                "#,
-                glue = dioxus_liveview::interpreter_glue(&format!("ws://{listen_address}/ws"))
-            ))
-        }))
-        .route(
-            "/ws",
-            get(move |ws: WebSocketUpgrade| async move {
-                ws.on_upgrade(move |socket| async move {
-                    _ = view.launch(dioxus_liveview::axum_socket(socket), app).await;
-                })
-            }),
-        );
-
-    println!("Listening on http://{listen_address}");
-
-    let listener = tokio::net::TcpListener::bind(&listen_address)
-        .await
-        .unwrap();
-
-    axum::serve(listener, app.into_make_service())
-        .await
-        .unwrap();
-}
-
-#[cfg(not(feature = "liveview"))]
-fn main() {
-    dioxus::launch(app)
-}
-
-fn app() -> Element {
-    rsx! {
-        Router::<Route> {}
-    }
-}
-
-#[component]
-fn UserFrame(user_id: usize) -> Element {
-    rsx! {
-        pre { "UserFrame{{\n\tuser_id:{user_id}\n}}" }
-        div { background_color: "rgba(0,0,0,50%)",
-            "children:"
-            Outlet::<Route> {}
-        }
-    }
-}
-
-#[component]
-fn Route1(user_id: usize, dynamic: usize, query: String) -> Element {
-    rsx! {
-        pre {
-            "Route1{{\n\tuser_id:{user_id},\n\tdynamic:{dynamic},\n\tquery:{query}\n}}"
-        }
-        Link {
-            to: Route::Route1 {
-                user_id,
-                dynamic,
-                query: String::new(),
-            },
-            "Route1 with extra+\".\""
-        }
-        p { "Footer" }
-        Link {
-            to: Route::Route3 {
-                dynamic: String::new(),
-            },
-            "Home"
-        }
-    }
-}
-
-#[component]
-fn Route2(user_id: usize) -> Element {
-    rsx! {
-        pre { "Route2{{\n\tuser_id:{user_id}\n}}" }
-        {(0..user_id).map(|i| rsx! { p { "{i}" } })}
-        p { "Footer" }
-        Link {
-            to: Route::Route3 {
-                dynamic: String::new(),
-            },
-            "Home"
-        }
-    }
-}
-
-#[component]
-fn Route3(dynamic: String) -> Element {
-    let mut current_route_str = use_signal(String::new);
-
-    let current_route = use_route();
-    let parsed = Route::from_str(&current_route_str.read());
-
-    let site_map = Route::SITE_MAP
-        .iter()
-        .flat_map(|seg| seg.flatten().into_iter())
-        .collect::<Vec<_>>();
-
-    let navigator = use_navigator();
-
-    rsx! {
-        input {
-            oninput: move |evt: FormEvent| {
-                *current_route_str.write() = evt.value();
-            },
-            value: "{current_route_str}"
-        }
-        "dynamic: {dynamic}"
-        Link { to: Route::Route2 { user_id: 8888 }, "hello world link" }
-        button {
-            disabled: !navigator.can_go_back(),
-            onclick: move |_| {
-                navigator.go_back();
-            },
-            "go back"
-        }
-        button {
-            disabled: !navigator.can_go_forward(),
-            onclick: move |_| {
-                navigator.go_forward();
-            },
-            "go forward"
-        }
-        button {
-            onclick: move |_| {
-                navigator.push("https://www.google.com");
-            },
-            "google link"
-        }
-        p { "Site Map" }
-        pre { "{site_map:#?}" }
-        p { "Dynamic link" }
-        match parsed {
-            Ok(route) => {
-                if route != current_route {
-                    rsx! {
-                        Link {
-                            to: route.clone(),
-                            "{route}"
-                        }
-                    }
-                }
-                else {
-                    VNode::empty()
-                }
-            }
-            Err(err) => {
-                rsx! {
-                    pre {
-                        color: "red",
-                        "Invalid route:\n{err}"
-                    }
-                }
-            }
-        }
-    }
-}
-
-#[rustfmt::skip]
-#[derive(Clone, Debug, PartialEq, Routable)]
-enum Route {
-    #[nest("/test")]
-        // Nests with parameters have types taken from child routes
-        #[nest("/user/:user_id")]
-            // Everything inside the nest has the added parameter `user_id: usize`
-            // UserFrame is a layout component that will receive the `user_id: usize` parameter
-            #[layout(UserFrame)]
-                #[route("/:dynamic?:query")]
-                Route1 {
-                    // The type is taken from the first instance of the dynamic parameter
-                    user_id: usize,
-                    dynamic: usize,
-                    query: String,
-                },
-                #[route("/hello_world")]
-                // You can opt out of the layout by using the `!` prefix
-                #[layout(!UserFrame)]
-                Route2 { user_id: usize },
-            #[end_layout]
-        #[end_nest]
-    #[end_nest]
-    #[redirect("/:id/user", |id: usize| Route::Route3 { dynamic: id.to_string()})]
-    #[route("/:dynamic")]
-    Route3 { dynamic: String },
-}

+ 68 - 0
packages/router/src/components/child_router.rs

@@ -0,0 +1,68 @@
+/// Components that allow the macro to add child routers. This component provides a context
+/// to the child router that maps child routes to root routes and vice versa.
+use dioxus_lib::prelude::*;
+
+use crate::prelude::Routable;
+
+/// Maps a child route into the root router and vice versa
+// NOTE: Currently child routers only support simple static prefixes, but this
+// API could be expanded to support dynamic prefixes as well
+pub(crate) struct ChildRouteMapping<R> {
+    format_route_as_root_route: fn(R) -> String,
+    parse_route_from_root_route: fn(&str) -> Option<R>,
+}
+
+impl<R: Routable> ChildRouteMapping<R> {
+    pub(crate) fn format_route_as_root_route(&self, route: R) -> String {
+        (self.format_route_as_root_route)(route)
+    }
+
+    pub(crate) fn parse_route_from_root_route(&self, route: &str) -> Option<R> {
+        (self.parse_route_from_root_route)(route)
+    }
+}
+
+/// Get the formatter that handles adding and stripping the prefix from a child route
+pub(crate) fn consume_child_route_mapping<R: Routable>() -> Option<ChildRouteMapping<R>> {
+    try_consume_context()
+}
+
+impl<R> Clone for ChildRouteMapping<R> {
+    fn clone(&self) -> Self {
+        Self {
+            format_route_as_root_route: self.format_route_as_root_route,
+            parse_route_from_root_route: self.parse_route_from_root_route,
+        }
+    }
+}
+
+/// Props for the [`ChildHistoryProvider`] component.
+#[derive(Props, Clone)]
+pub struct ChildRouterProps<R: Routable> {
+    /// The child route to render
+    route: R,
+    /// Take a parent route and return a child route or none if the route is not part of the child
+    parse_route_from_root_route: fn(&str) -> Option<R>,
+    /// Take a child route and return a parent route
+    format_route_as_root_route: fn(R) -> String,
+}
+
+impl<R: Routable> PartialEq for ChildRouterProps<R> {
+    fn eq(&self, _: &Self) -> bool {
+        false
+    }
+}
+
+/// A component that provides a [`History`] to a child router. The `#[child]` attribute on the router macro will insert this automatically.
+#[component]
+#[allow(missing_docs)]
+pub fn ChildRouter<R: Routable>(props: ChildRouterProps<R>) -> Element {
+    use_hook(|| {
+        provide_context(ChildRouteMapping {
+            format_route_as_root_route: props.format_route_as_root_route,
+            parse_route_from_root_route: props.parse_route_from_root_route,
+        })
+    });
+
+    props.route.render(0)
+}

+ 20 - 0
packages/router/src/components/history_provider.rs

@@ -0,0 +1,20 @@
+use dioxus_history::{provide_history_context, History};
+use dioxus_lib::prelude::*;
+
+use std::rc::Rc;
+
+/// A component that provides a [`History`] for all child [`Router`] components. Renderers generally provide a default history automatically.
+#[component]
+#[allow(missing_docs)]
+pub fn HistoryProvider(
+    /// The history to provide to child components.
+    history: Callback<(), Rc<dyn History>>,
+    /// The children to render within the history provider.
+    children: Element,
+) -> Element {
+    use_hook(|| {
+        provide_history_context(history(()));
+    });
+
+    children
+}

+ 8 - 78
packages/router/src/components/link.rs

@@ -1,83 +1,14 @@
 #![allow(clippy::type_complexity)]
 
-use std::any::Any;
 use std::fmt::Debug;
-use std::rc::Rc;
 
 use dioxus_lib::prelude::*;
 
 use tracing::error;
 
 use crate::navigation::NavigationTarget;
-use crate::prelude::Routable;
 use crate::utils::use_router_internal::use_router_internal;
 
-use url::Url;
-
-/// Something that can be converted into a [`NavigationTarget`].
-#[derive(Clone)]
-pub enum IntoRoutable {
-    /// A raw string target.
-    FromStr(String),
-    /// A internal target.
-    Route(Rc<dyn Any>),
-}
-
-impl PartialEq for IntoRoutable {
-    fn eq(&self, other: &Self) -> bool {
-        match (self, other) {
-            (IntoRoutable::FromStr(a), IntoRoutable::FromStr(b)) => a == b,
-            (IntoRoutable::Route(a), IntoRoutable::Route(b)) => Rc::ptr_eq(a, b),
-            _ => false,
-        }
-    }
-}
-
-impl<R: Routable> From<R> for IntoRoutable {
-    fn from(value: R) -> Self {
-        IntoRoutable::Route(Rc::new(value) as Rc<dyn Any>)
-    }
-}
-
-impl<R: Routable> From<NavigationTarget<R>> for IntoRoutable {
-    fn from(value: NavigationTarget<R>) -> Self {
-        match value {
-            NavigationTarget::Internal(route) => IntoRoutable::Route(Rc::new(route) as Rc<dyn Any>),
-            NavigationTarget::External(url) => IntoRoutable::FromStr(url),
-        }
-    }
-}
-
-impl From<String> for IntoRoutable {
-    fn from(value: String) -> Self {
-        IntoRoutable::FromStr(value)
-    }
-}
-
-impl From<&String> for IntoRoutable {
-    fn from(value: &String) -> Self {
-        IntoRoutable::FromStr(value.to_string())
-    }
-}
-
-impl From<&str> for IntoRoutable {
-    fn from(value: &str) -> Self {
-        IntoRoutable::FromStr(value.to_string())
-    }
-}
-
-impl From<Url> for IntoRoutable {
-    fn from(url: Url) -> Self {
-        IntoRoutable::FromStr(url.to_string())
-    }
-}
-
-impl From<&Url> for IntoRoutable {
-    fn from(url: &Url) -> Self {
-        IntoRoutable::FromStr(url.to_string())
-    }
-}
-
 /// The properties for a [`Link`].
 #[derive(Props, Clone, PartialEq)]
 pub struct LinkProps {
@@ -119,7 +50,7 @@ pub struct LinkProps {
 
     /// The navigation target. Roughly equivalent to the href attribute of an HTML anchor tag.
     #[props(into)]
-    pub to: IntoRoutable,
+    pub to: NavigationTarget,
 
     #[props(extends = GlobalAttributes)]
     attributes: Vec<Attribute>,
@@ -227,12 +158,11 @@ pub fn Link(props: LinkProps) -> Element {
         }
     };
 
-    let current_url = router.current_route_string();
+    let current_url = router.full_route_string();
     let href = match &to {
-        IntoRoutable::FromStr(url) => url.to_string(),
-        IntoRoutable::Route(route) => router.any_route_to_string(&**route),
+        NavigationTarget::Internal(url) => url.clone(),
+        NavigationTarget::External(route) => route.clone(),
     };
-    let parsed_route: NavigationTarget<Rc<dyn Any>> = router.resolve_into_routable(to.clone());
 
     let mut class_ = String::new();
     if let Some(c) = class {
@@ -257,7 +187,7 @@ pub fn Link(props: LinkProps) -> Element {
 
     let tag_target = new_tab.then_some("_blank");
 
-    let is_external = matches!(parsed_route, NavigationTarget::External(_));
+    let is_external = matches!(to, NavigationTarget::External(_));
     let is_router_nav = !is_external && !new_tab;
     let rel = rel.or_else(|| is_external.then_some("noopener noreferrer".to_string()));
 
@@ -281,7 +211,7 @@ pub fn Link(props: LinkProps) -> Element {
         event.prevent_default();
 
         if do_default && is_router_nav {
-            router.push_any(router.resolve_into_routable(to.clone()));
+            router.push_any(to.clone());
         }
 
         if let Some(handler) = onclick {
@@ -301,8 +231,8 @@ pub fn Link(props: LinkProps) -> Element {
     let liveview_prevent_default = {
         // If the event is a click with the left mouse button and no modifiers, prevent the default action
         // and navigate to the href with client side routing
-        router.is_liveview().then_some(
-            "if (event.button === 0 && !event.ctrlKey && !event.metaKey && !event.shiftKey && !event.altKey) { event.preventDefault() }"
+        router.include_prevent_default().then_some(
+            "if (event.button === 0 && !event.ctrlKey && !event.metaKey && !event.shiftKey && !event.altKey) { event.preventDefault() }"   
         )
     };
 

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

@@ -58,8 +58,9 @@ use dioxus_lib::prelude::*;
 /// # #[component]
 /// # fn App() -> Element {
 /// #     rsx! {
-/// #         Router::<Route> {
-/// #             config: || RouterConfig::default().history(MemoryHistory::with_initial_path(Route::Child {}))
+/// #         dioxus_router::components::HistoryProvider {
+/// #             history:  move |_| std::rc::Rc::new(dioxus_history::MemoryHistory::with_initial_path(Route::Child {}.to_string())) as std::rc::Rc<dyn dioxus_history::History>,
+/// #             Router::<Route> {}
 /// #         }
 /// #     }
 /// # }

+ 1 - 4
packages/router/src/components/router.rs

@@ -46,10 +46,7 @@ where
     use crate::prelude::{outlet::OutletContext, RouterContext};
 
     use_hook(|| {
-        provide_router_context(RouterContext::new(
-            props.config.call(()),
-            schedule_update_any(),
-        ));
+        provide_router_context(RouterContext::new(props.config.call(())));
 
         provide_context(OutletContext::<R> {
             current_level: 0,

+ 6 - 3
packages/router/src/contexts/navigator.rs

@@ -1,4 +1,4 @@
-use crate::prelude::{ExternalNavigationFailure, IntoRoutable, RouterContext};
+use crate::prelude::{ExternalNavigationFailure, NavigationTarget, RouterContext};
 
 /// Acquire the navigator without subscribing to updates.
 ///
@@ -48,14 +48,17 @@ impl Navigator {
     /// Push a new location.
     ///
     /// The previous location will be available to go back to.
-    pub fn push(&self, target: impl Into<IntoRoutable>) -> Option<ExternalNavigationFailure> {
+    pub fn push(&self, target: impl Into<NavigationTarget>) -> Option<ExternalNavigationFailure> {
         self.0.push(target)
     }
 
     /// Replace the current location.
     ///
     /// The previous location will **not** be available to go back to.
-    pub fn replace(&self, target: impl Into<IntoRoutable>) -> Option<ExternalNavigationFailure> {
+    pub fn replace(
+        &self,
+        target: impl Into<NavigationTarget>,
+    ) -> Option<ExternalNavigationFailure> {
         self.0.replace(target)
     }
 }

+ 87 - 143
packages/router/src/contexts/router.rs

@@ -1,17 +1,14 @@
 use std::{
-    any::Any,
     collections::HashSet,
-    rc::Rc,
-    sync::{Arc, RwLock},
+    sync::{Arc, Mutex},
 };
 
+use dioxus_history::history;
 use dioxus_lib::prelude::*;
 
 use crate::{
-    navigation::NavigationTarget,
-    prelude::{AnyHistoryProvider, IntoRoutable, SiteMapSegment},
-    routable::Routable,
-    router_cfg::RouterConfig,
+    components::child_router::consume_child_route_mapping, navigation::NavigationTarget,
+    prelude::SiteMapSegment, routable::Routable, router_cfg::RouterConfig,
 };
 
 /// This context is set in the root of the virtual dom if there is a router present.
@@ -47,38 +44,39 @@ pub struct ExternalNavigationFailure(pub String);
 /// A function the router will call after every routing update.
 pub(crate) type RoutingCallback<R> =
     Arc<dyn Fn(GenericRouterContext<R>) -> Option<NavigationTarget<R>>>;
-pub(crate) type AnyRoutingCallback =
-    Arc<dyn Fn(RouterContext) -> Option<NavigationTarget<Rc<dyn Any>>>>;
+pub(crate) type AnyRoutingCallback = Arc<dyn Fn(RouterContext) -> Option<NavigationTarget>>;
 
 struct RouterContextInner {
     /// The current prefix.
     prefix: Option<String>,
 
-    history: Box<dyn AnyHistoryProvider>,
-
     unresolved_error: Option<ExternalNavigationFailure>,
 
-    subscribers: Arc<RwLock<HashSet<ScopeId>>>,
-    subscriber_update: Arc<dyn Fn(ScopeId)>,
+    subscribers: Arc<Mutex<HashSet<ReactiveContext>>>,
     routing_callback: Option<AnyRoutingCallback>,
 
     failure_external_navigation: fn() -> Element,
 
-    any_route_to_string: fn(&dyn Any) -> String,
+    internal_route: fn(&str) -> bool,
 
     site_map: &'static [SiteMapSegment],
 }
 
 impl RouterContextInner {
     fn update_subscribers(&self) {
-        let update = &self.subscriber_update;
-        for &id in self.subscribers.read().unwrap().iter() {
-            update(id);
+        for &id in self.subscribers.lock().unwrap().iter() {
+            id.mark_dirty();
+        }
+    }
+
+    fn subscribe_to_current_context(&self) {
+        if let Some(rc) = ReactiveContext::current() {
+            rc.subscribe(self.subscribers.clone());
         }
     }
 
     fn external(&mut self, external: String) -> Option<ExternalNavigationFailure> {
-        match self.history.external(external.clone()) {
+        match history().external(external.clone()) {
             true => None,
             false => {
                 let failure = ExternalNavigationFailure(external);
@@ -99,23 +97,17 @@ pub struct RouterContext {
 }
 
 impl RouterContext {
-    pub(crate) fn new<R: Routable + 'static>(
-        mut cfg: RouterConfig<R>,
-        mark_dirty: Arc<dyn Fn(ScopeId) + Sync + Send>,
-    ) -> Self
+    pub(crate) fn new<R: Routable + 'static>(cfg: RouterConfig<R>) -> Self
     where
         <R as std::str::FromStr>::Err: std::fmt::Display,
     {
-        let subscriber_update = mark_dirty.clone();
-        let subscribers = Arc::new(RwLock::new(HashSet::new()));
+        let subscribers = Arc::new(Mutex::new(HashSet::new()));
+        let mapping = consume_child_route_mapping();
 
-        let mut myself = RouterContextInner {
+        let myself = RouterContextInner {
             prefix: Default::default(),
-            history: cfg.take_history(),
             unresolved_error: None,
             subscribers: subscribers.clone(),
-            subscriber_update,
-
             routing_callback: cfg.on_update.map(|update| {
                 Arc::new(move |ctx| {
                     let ctx = GenericRouterContext {
@@ -123,42 +115,30 @@ impl RouterContext {
                         _marker: std::marker::PhantomData,
                     };
                     update(ctx).map(|t| match t {
-                        NavigationTarget::Internal(r) => {
-                            NavigationTarget::Internal(Rc::new(r) as Rc<dyn Any>)
-                        }
+                        NavigationTarget::Internal(r) => match mapping.as_ref() {
+                            Some(mapping) => {
+                                NavigationTarget::Internal(mapping.format_route_as_root_route(r))
+                            }
+                            None => NavigationTarget::Internal(r.to_string()),
+                        },
                         NavigationTarget::External(s) => NavigationTarget::External(s),
                     })
-                })
-                    as Arc<dyn Fn(RouterContext) -> Option<NavigationTarget<Rc<dyn Any>>>>
+                }) as Arc<dyn Fn(RouterContext) -> Option<NavigationTarget>>
             }),
 
             failure_external_navigation: cfg.failure_external_navigation,
 
-            any_route_to_string: |route| {
-                route
-                    .downcast_ref::<R>()
-                    .unwrap_or_else(|| {
-                        panic!(
-                            "Route is not of the expected type: {}\n found typeid: {:?}\n expected typeid: {:?}",
-                            std::any::type_name::<R>(),
-                            route.type_id(),
-                            std::any::TypeId::of::<R>()
-                        )
-                    })
-                    .to_string()
-            },
+            internal_route: |route| R::from_str(route).is_ok(),
 
             site_map: R::SITE_MAP,
         };
 
         // set the updater
-        {
-            myself.history.updater(Arc::new(move || {
-                for &id in subscribers.read().unwrap().iter() {
-                    (mark_dirty)(id);
-                }
-            }));
-        }
+        history().updater(Arc::new(move || {
+            for &rc in subscribers.lock().unwrap().iter() {
+                rc.mark_dirty();
+            }
+        }));
 
         Self {
             inner: CopyValue::new_in_scope(myself, ScopeId::ROOT),
@@ -167,41 +147,27 @@ impl RouterContext {
 
     /// Check if the router is running in a liveview context
     /// We do some slightly weird things for liveview because of the network boundary
-    pub fn is_liveview(&self) -> bool {
-        #[cfg(feature = "liveview")]
-        {
-            self.inner.read().history.is_liveview()
-        }
-        #[cfg(not(feature = "liveview"))]
-        {
-            false
-        }
-    }
-
-    pub(crate) fn route_from_str(&self, route: &str) -> Result<Rc<dyn Any>, String> {
-        self.inner.read().history.parse_route(route)
+    pub(crate) fn include_prevent_default(&self) -> bool {
+        history().include_prevent_default()
     }
 
     /// Check whether there is a previous page to navigate back to.
     #[must_use]
     pub fn can_go_back(&self) -> bool {
-        self.inner.read().history.can_go_back()
+        history().can_go_back()
     }
 
     /// Check whether there is a future page to navigate forward to.
     #[must_use]
     pub fn can_go_forward(&self) -> bool {
-        self.inner.read().history.can_go_forward()
+        history().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.inner.write_unchecked().history.go_back();
-        }
-
+        history().go_back();
         self.change_route();
     }
 
@@ -209,21 +175,15 @@ impl RouterContext {
     ///
     /// Will fail silently if there is no next location to go to.
     pub fn go_forward(&self) {
-        {
-            self.inner.write_unchecked().history.go_forward();
-        }
-
+        history().go_forward();
         self.change_route();
     }
 
-    pub(crate) fn push_any(
-        &self,
-        target: NavigationTarget<Rc<dyn Any>>,
-    ) -> Option<ExternalNavigationFailure> {
+    pub(crate) fn push_any(&self, target: NavigationTarget) -> Option<ExternalNavigationFailure> {
         {
             let mut write = self.inner.write_unchecked();
             match target {
-                NavigationTarget::Internal(p) => write.history.push(p),
+                NavigationTarget::Internal(p) => history().push(p),
                 NavigationTarget::External(e) => return write.external(e),
             }
         }
@@ -234,12 +194,15 @@ impl RouterContext {
     /// Push a new location.
     ///
     /// The previous location will be available to go back to.
-    pub fn push(&self, target: impl Into<IntoRoutable>) -> Option<ExternalNavigationFailure> {
-        let target = self.resolve_into_routable(target.into());
+    pub fn push(&self, target: impl Into<NavigationTarget>) -> Option<ExternalNavigationFailure> {
+        let target = target.into();
         {
             let mut write = self.inner.write_unchecked();
             match target {
-                NavigationTarget::Internal(p) => write.history.push(p),
+                NavigationTarget::Internal(p) => {
+                    let history = history();
+                    history.push(p)
+                }
                 NavigationTarget::External(e) => return write.external(e),
             }
         }
@@ -250,13 +213,18 @@ impl RouterContext {
     /// Replace the current location.
     ///
     /// The previous location will **not** be available to go back to.
-    pub fn replace(&self, target: impl Into<IntoRoutable>) -> Option<ExternalNavigationFailure> {
-        let target = self.resolve_into_routable(target.into());
-
+    pub fn replace(
+        &self,
+        target: impl Into<NavigationTarget>,
+    ) -> Option<ExternalNavigationFailure> {
+        let target = target.into();
         {
             let mut state = self.inner.write_unchecked();
             match target {
-                NavigationTarget::Internal(p) => state.history.replace(p),
+                NavigationTarget::Internal(p) => {
+                    let history = history();
+                    history.replace(p)
+                }
                 NavigationTarget::External(e) => return state.external(e),
             }
         }
@@ -266,39 +234,27 @@ impl RouterContext {
 
     /// The route that is currently active.
     pub fn current<R: Routable>(&self) -> R {
-        self.inner
-            .read()
-            .history
-            .current_route()
-            .downcast::<R>()
-            .unwrap()
-            .as_ref()
-            .clone()
-    }
-
-    /// The route that is currently active.
-    pub fn current_route_string(&self) -> String {
-        self.any_route_to_string(&*self.inner.read().history.current_route())
+        let absolute_route = self.full_route_string();
+        // If this is a child route, map the absolute route to the child route before parsing
+        let mapping = consume_child_route_mapping::<R>();
+        match mapping.as_ref() {
+            Some(mapping) => mapping
+                .parse_route_from_root_route(&absolute_route)
+                .unwrap_or_else(|| {
+                    panic!("route's display implementation must be parsable by FromStr")
+                }),
+            None => R::from_str(&absolute_route).unwrap_or_else(|_| {
+                panic!("route's display implementation must be parsable by FromStr")
+            }),
+        }
     }
 
-    pub(crate) fn any_route_to_string(&self, route: &dyn Any) -> String {
-        (self.inner.read().any_route_to_string)(route)
-    }
-
-    pub(crate) fn resolve_into_routable(
-        &self,
-        into_routable: IntoRoutable,
-    ) -> NavigationTarget<Rc<dyn Any>> {
-        match into_routable {
-            IntoRoutable::FromStr(url) => {
-                let parsed_route: NavigationTarget<Rc<dyn Any>> = match self.route_from_str(&url) {
-                    Ok(route) => NavigationTarget::Internal(route),
-                    Err(_) => NavigationTarget::External(url),
-                };
-                parsed_route
-            }
-            IntoRoutable::Route(route) => NavigationTarget::Internal(route),
-        }
+    /// The full route that is currently active. If this is called from inside a child router, this will always return the parent's view of the route.
+    pub fn full_route_string(&self) -> String {
+        let inner = self.inner.read();
+        inner.subscribe_to_current_context();
+        let history = history();
+        history.current_route()
     }
 
     /// The prefix that is currently active.
@@ -306,16 +262,6 @@ impl RouterContext {
         self.inner.read().prefix.clone()
     }
 
-    /// Manually subscribe to the current route
-    pub fn subscribe(&self, id: ScopeId) {
-        self.inner.read().subscribers.write().unwrap().insert(id);
-    }
-
-    /// Manually unsubscribe from the current route
-    pub fn unsubscribe(&self, id: ScopeId) {
-        self.inner.read().subscribers.write().unwrap().remove(&id);
-    }
-
     /// Clear any unresolved errors
     pub fn clear_error(&self) {
         let mut write_inner = self.inner.write_unchecked();
@@ -330,11 +276,12 @@ impl RouterContext {
     }
 
     pub(crate) fn render_error(&self) -> Option<Element> {
-        let inner_read = self.inner.write_unchecked();
-        inner_read
+        let inner_write = self.inner.write_unchecked();
+        inner_write.subscribe_to_current_context();
+        inner_write
             .unresolved_error
             .as_ref()
-            .map(|_| (inner_read.failure_external_navigation)())
+            .map(|_| (inner_write.failure_external_navigation)())
     }
 
     fn change_route(&self) -> Option<ExternalNavigationFailure> {
@@ -346,7 +293,10 @@ impl RouterContext {
             if let Some(new) = callback(myself) {
                 let mut self_write = self.inner.write_unchecked();
                 match new {
-                    NavigationTarget::Internal(p) => self_write.history.replace(p),
+                    NavigationTarget::Internal(p) => {
+                        let history = history();
+                        history.replace(p)
+                    }
                     NavigationTarget::External(e) => return self_write.external(e),
                 }
             }
@@ -356,6 +306,10 @@ impl RouterContext {
 
         None
     }
+
+    pub(crate) fn internal_route(&self, route: &str) -> bool {
+        (self.inner.read().internal_route)(route)
+    }
 }
 
 pub struct GenericRouterContext<R> {
@@ -426,16 +380,6 @@ where
         self.inner.prefix()
     }
 
-    /// Manually subscribe to the current route
-    pub fn subscribe(&self, id: ScopeId) {
-        self.inner.subscribe(id)
-    }
-
-    /// Manually unsubscribe from the current route
-    pub fn unsubscribe(&self, id: ScopeId) {
-        self.inner.unsubscribe(id)
-    }
-
     /// Clear any unresolved errors
     pub fn clear_error(&self) {
         self.inner.clear_error()

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

@@ -1,103 +0,0 @@
-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: R,
-    history: Vec<R>,
-    future: Vec<R>,
-}
-
-impl<R: Routable> MemoryHistory<R>
-where
-    <R as FromStr>::Err: std::fmt::Display,
-{
-    /// Create a [`MemoryHistory`] starting at `path`.
-    ///
-    /// ```rust
-    /// # use dioxus_router::prelude::*;
-    /// # use dioxus::prelude::*;
-    /// # #[component]
-    /// # fn Index() -> Element { VNode::empty() }
-    /// # #[component]
-    /// # fn OtherPage() -> Element { VNode::empty() }
-    /// #[derive(Clone, Routable, Debug, PartialEq)]
-    /// enum Route {
-    ///     #[route("/")]
-    ///     Index {},
-    ///     #[route("/some-other-page")]
-    ///     OtherPage {},
-    /// }
-    ///
-    /// let mut history = MemoryHistory::<Route>::with_initial_path(Route::Index {});
-    /// assert_eq!(history.current_route(), Route::Index {});
-    /// assert_eq!(history.can_go_back(), false);
-    /// ```
-    pub fn with_initial_path(path: R) -> Self {
-        Self {
-            current: path,
-            history: Vec::new(),
-            future: Vec::new(),
-        }
-    }
-}
-
-impl<R: Routable> Default for MemoryHistory<R>
-where
-    <R as FromStr>::Err: std::fmt::Display,
-{
-    fn default() -> Self {
-        Self {
-            current: "/".parse().unwrap_or_else(|err| {
-                panic!("index route does not exist:\n{err}\n use MemoryHistory::with_initial_path to set a custom path")
-            }),
-            history: Vec::new(),
-            future: Vec::new(),
-        }
-    }
-}
-
-impl<R: Routable> HistoryProvider<R> for MemoryHistory<R> {
-    fn current_route(&self) -> R {
-        self.current.clone()
-    }
-
-    fn can_go_back(&self) -> bool {
-        !self.history.is_empty()
-    }
-
-    fn go_back(&mut self) {
-        if let Some(last) = self.history.pop() {
-            let old = std::mem::replace(&mut self.current, last);
-            self.future.push(old);
-        }
-    }
-
-    fn can_go_forward(&self) -> bool {
-        !self.future.is_empty()
-    }
-
-    fn go_forward(&mut self) {
-        if let Some(next) = self.future.pop() {
-            let old = std::mem::replace(&mut self.current, next);
-            self.history.push(old);
-        }
-    }
-
-    fn push(&mut self, new: R) {
-        // don't push the same route twice
-        if self.current.to_string() == new.to_string() {
-            return;
-        }
-        let old = std::mem::replace(&mut self.current, new);
-        self.history.push(old);
-        self.future.clear();
-    }
-
-    fn replace(&mut self, path: R) {
-        self.current = path;
-    }
-}

+ 0 - 211
packages/router/src/history/web_hash.rs

@@ -1,211 +0,0 @@
-use std::sync::{Arc, Mutex};
-
-use gloo::{events::EventListener, render::AnimationFrame, utils::window};
-use serde::{de::DeserializeOwned, Serialize};
-use tracing::error;
-use url::Url;
-use web_sys::{History, ScrollRestoration, Window};
-
-use crate::routable::Routable;
-
-use super::HistoryProvider;
-
-const INITIAL_URL: &str = "dioxus-router-core://initial_url.invalid/";
-
-/// A [`HistoryProvider`] that integrates with a browser via the [History API]. It uses the URLs
-/// hash instead of its path.
-///
-/// Early web applications used the hash to store the current path because there was no other way
-/// for them to interact with the history without triggering a browser navigation, as the
-/// [History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API) did not yet exist. While this implementation could have been written that way, it
-/// was not, because no browser supports WebAssembly without the [History API].
-pub struct WebHashHistory<R: Serialize + DeserializeOwned> {
-    do_scroll_restoration: bool,
-    history: History,
-    listener_navigation: Option<EventListener>,
-    #[allow(dead_code)]
-    listener_scroll: Option<EventListener>,
-    listener_animation_frame: Arc<Mutex<Option<AnimationFrame>>>,
-    window: Window,
-    phantom: std::marker::PhantomData<R>,
-}
-
-impl<R: Serialize + DeserializeOwned> WebHashHistory<R> {
-    /// Create a new [`WebHashHistory`].
-    ///
-    /// If `do_scroll_restoration` is [`true`], [`WebHashHistory`] will take control of the history
-    /// state. It'll also set the browsers scroll restoration to `manual`.
-    pub fn new(do_scroll_restoration: bool) -> Self {
-        let window = window();
-        let history = window.history().expect("`window` has access to `history`");
-
-        history
-            .set_scroll_restoration(ScrollRestoration::Manual)
-            .expect("`history` can set scroll restoration");
-
-        let listener_scroll = match do_scroll_restoration {
-            true => {
-                history
-                    .set_scroll_restoration(ScrollRestoration::Manual)
-                    .expect("`history` can set scroll restoration");
-                let w = window.clone();
-                let h = history.clone();
-                let document = w.document().expect("`window` has access to `document`");
-
-                Some(EventListener::new(&document, "scroll", move |_| {
-                    update_history(&w, &h);
-                }))
-            }
-            false => None,
-        };
-
-        Self {
-            do_scroll_restoration,
-            history,
-            listener_navigation: None,
-            listener_scroll,
-            listener_animation_frame: Default::default(),
-            window,
-            phantom: Default::default(),
-        }
-    }
-}
-
-impl<R: Serialize + DeserializeOwned> WebHashHistory<R> {
-    fn join_url_to_hash(&self, path: R) -> Option<String> {
-        let url = match self.url() {
-            Some(c) => match c.join(&path) {
-                Ok(new) => new,
-                Err(e) => {
-                    error!("failed to join location with target: {e}");
-                    return None;
-                }
-            },
-            None => {
-                error!("current location unknown");
-                return None;
-            }
-        };
-
-        Some(format!(
-            "#{path}{query}",
-            path = url.path(),
-            query = url.query().map(|q| format!("?{q}")).unwrap_or_default()
-        ))
-    }
-
-    fn url(&self) -> Option<Url> {
-        let mut path = self.window.location().hash().ok()?;
-
-        if path.starts_with('#') {
-            path.remove(0);
-        }
-
-        if path.starts_with('/') {
-            path.remove(0);
-        }
-
-        match Url::parse(&format!("{INITIAL_URL}/{path}")) {
-            Ok(url) => Some(url),
-            Err(e) => {
-                error!("failed to parse hash path: {e}");
-                None
-            }
-        }
-    }
-}
-
-impl<R: Serialize + DeserializeOwned + Routable> HistoryProvider<R> for WebHashHistory<R> {
-    fn current_route(&self) -> R {
-        self.url()
-            .map(|url| url.path().to_string())
-            .unwrap_or(String::from("/"))
-    }
-
-    fn current_prefix(&self) -> Option<String> {
-        Some(String::from("#"))
-    }
-
-    fn go_back(&mut self) {
-        if let Err(e) = self.history.back() {
-            error!("failed to go back: {e:?}")
-        }
-    }
-
-    fn go_forward(&mut self) {
-        if let Err(e) = self.history.forward() {
-            error!("failed to go forward: {e:?}")
-        }
-    }
-
-    fn push(&mut self, path: R) {
-        let hash = match self.join_url_to_hash(path) {
-            Some(hash) => hash,
-            None => return,
-        };
-
-        let state = match self.do_scroll_restoration {
-            true => top_left(),
-            false => self.history.state().unwrap_or_default(),
-        };
-
-        let nav = self.history.push_state_with_url(&state, "", Some(&hash));
-
-        match nav {
-            Ok(_) => {
-                if self.do_scroll_restoration {
-                    self.window.scroll_to_with_x_and_y(0.0, 0.0)
-                }
-            }
-            Err(e) => error!("failed to push state: {e:?}"),
-        }
-    }
-
-    fn replace(&mut self, path: R) {
-        let hash = match self.join_url_to_hash(path) {
-            Some(hash) => hash,
-            None => return,
-        };
-
-        let state = match self.do_scroll_restoration {
-            true => top_left(),
-            false => self.history.state().unwrap_or_default(),
-        };
-
-        let nav = self.history.replace_state_with_url(&state, "", Some(&hash));
-
-        match nav {
-            Ok(_) => {
-                if self.do_scroll_restoration {
-                    self.window.scroll_to_with_x_and_y(0.0, 0.0)
-                }
-            }
-            Err(e) => error!("failed to replace state: {e:?}"),
-        }
-    }
-
-    fn external(&mut self, url: String) -> bool {
-        match self.window.location().set_href(&url) {
-            Ok(_) => true,
-            Err(e) => {
-                error!("failed to navigate to external url (`{url}): {e:?}");
-                false
-            }
-        }
-    }
-
-    fn updater(&mut self, callback: std::sync::Arc<dyn Fn() + Send + Sync>) {
-        let w = self.window.clone();
-        let h = self.history.clone();
-        let s = self.listener_animation_frame.clone();
-        let d = self.do_scroll_restoration;
-
-        self.listener_navigation = Some(EventListener::new(&self.window, "popstate", move |_| {
-            (*callback)();
-            if d {
-                let mut s = s.lock().expect("unpoisoned scroll mutex");
-                *s = Some(update_scroll(&w, &h));
-            }
-        }));
-    }
-}

+ 0 - 42
packages/router/src/history/web_history.rs

@@ -1,42 +0,0 @@
-use gloo::console::error;
-use wasm_bindgen::JsValue;
-use web_sys::History;
-
-pub(crate) fn replace_state_with_url(
-    history: &History,
-    value: &[f64; 2],
-    url: Option<&str>,
-) -> Result<(), JsValue> {
-    let position = js_sys::Array::new();
-    position.push(&JsValue::from(value[0]));
-    position.push(&JsValue::from(value[1]));
-
-    history.replace_state_with_url(&position, "", url)
-}
-
-pub(crate) fn push_state_and_url(
-    history: &History,
-    value: &[f64; 2],
-    url: String,
-) -> Result<(), JsValue> {
-    let position = js_sys::Array::new();
-    position.push(&JsValue::from(value[0]));
-    position.push(&JsValue::from(value[1]));
-
-    history.push_state_with_url(&position, "", Some(&url))
-}
-
-pub(crate) fn get_current(history: &History) -> Option<[f64; 2]> {
-    use wasm_bindgen::JsCast;
-
-    let state = history.state();
-    if let Err(err) = &state {
-        error!(err);
-    }
-    state.ok().and_then(|state| {
-        let state = state.dyn_into::<js_sys::Array>().ok()?;
-        let x = state.get(0).as_f64()?;
-        let y = state.get(1).as_f64()?;
-        Some([x, y])
-    })
-}

+ 0 - 22
packages/router/src/history/web_scroll.rs

@@ -1,22 +0,0 @@
-use gloo::render::{request_animation_frame, AnimationFrame};
-use web_sys::Window;
-
-#[derive(Clone, Copy, Debug, Default)]
-pub(crate) struct ScrollPosition {
-    pub x: f64,
-    pub y: f64,
-}
-
-impl ScrollPosition {
-    pub(crate) fn of_window(window: &Window) -> Self {
-        Self {
-            x: window.scroll_x().unwrap_or_default(),
-            y: window.scroll_y().unwrap_or_default(),
-        }
-    }
-
-    pub(crate) fn scroll_to(&self, window: Window) -> AnimationFrame {
-        let Self { x, y } = *self;
-        request_animation_frame(move |_| window.scroll_to_with_x_and_y(x, y))
-    }
-}

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

@@ -24,6 +24,12 @@ pub mod components {
 
     mod router;
     pub use router::*;
+
+    mod history_provider;
+    pub use history_provider::*;
+
+    #[doc(hidden)]
+    pub mod child_router;
 }
 
 mod contexts {
@@ -37,8 +43,6 @@ mod contexts {
 
 mod router_cfg;
 
-mod history;
-
 /// Hooks for interacting with the router in components.
 pub mod hooks {
     mod use_router;
@@ -55,9 +59,11 @@ pub use hooks::router;
 
 /// A collection of useful items most applications might need.
 pub mod prelude {
-    pub use crate::components::*;
+    pub use crate::components::{
+        GoBackButton, GoForwardButton, HistoryButtonProps, Link, LinkProps, Outlet, Router,
+        RouterProps,
+    };
     pub use crate::contexts::*;
-    pub use crate::history::*;
     pub use crate::hooks::*;
     pub use crate::navigation::*;
     pub use crate::routable::*;

+ 43 - 2
packages/router/src/navigation.rs

@@ -7,11 +7,23 @@ use std::{
 
 use url::{ParseError, Url};
 
-use crate::routable::Routable;
+use crate::{components::child_router::consume_child_route_mapping, routable::Routable, router};
+
+impl<R: Routable> From<R> for NavigationTarget {
+    fn from(value: R) -> Self {
+        // If this is a child route, map it to the root route first
+        let mapping = consume_child_route_mapping();
+        match mapping.as_ref() {
+            Some(mapping) => NavigationTarget::Internal(mapping.format_route_as_root_route(value)),
+            // Otherwise, just use the internal route
+            None => NavigationTarget::Internal(value.to_string()),
+        }
+    }
+}
 
 /// A target for the router to navigate to.
 #[derive(Clone, PartialEq, Eq, Debug)]
-pub enum NavigationTarget<R> {
+pub enum NavigationTarget<R = String> {
     /// An internal path that the router can navigate to by itself.
     ///
     /// ```rust
@@ -80,6 +92,35 @@ impl<R: Routable> From<R> for NavigationTarget<R> {
     }
 }
 
+impl From<&str> for NavigationTarget {
+    fn from(value: &str) -> Self {
+        let router = router();
+        match router.internal_route(value) {
+            true => NavigationTarget::Internal(value.to_string()),
+            false => NavigationTarget::External(value.to_string()),
+        }
+    }
+}
+
+impl From<String> for NavigationTarget {
+    fn from(value: String) -> Self {
+        let router = router();
+        match router.internal_route(&value) {
+            true => NavigationTarget::Internal(value),
+            false => NavigationTarget::External(value),
+        }
+    }
+}
+
+impl<R: Routable> From<NavigationTarget<R>> for NavigationTarget {
+    fn from(value: NavigationTarget<R>) -> Self {
+        match value {
+            NavigationTarget::Internal(r) => r.into(),
+            NavigationTarget::External(s) => Self::External(s),
+        }
+    }
+}
+
 impl<R: Routable> Display for NavigationTarget<R> {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         match self {

+ 9 - 84
packages/router/src/router_cfg.rs

@@ -1,4 +1,4 @@
-use crate::prelude::*;
+use crate::{components::FailureExternalNavigation, prelude::*};
 use dioxus_lib::prelude::*;
 use std::sync::Arc;
 
@@ -17,42 +17,29 @@ use std::sync::Arc;
 ///     #[route("/")]
 ///     Index {},
 /// }
-/// let cfg = RouterConfig::default().history(MemoryHistory::<Route>::default());
+///
+/// fn ExternalNavigationFailure() -> Element {
+///     rsx! {
+///         "Failed to navigate to external URL"
+///     }
+/// }
+///
+/// let cfg = RouterConfig::<Route>::default().failure_external_navigation(ExternalNavigationFailure);
 /// ```
 pub struct RouterConfig<R> {
     pub(crate) failure_external_navigation: fn() -> Element,
-    pub(crate) history: Option<Box<dyn AnyHistoryProvider>>,
     pub(crate) on_update: Option<RoutingCallback<R>>,
-    pub(crate) initial_route: Option<R>,
 }
 
 impl<R> Default for RouterConfig<R> {
     fn default() -> Self {
         Self {
             failure_external_navigation: FailureExternalNavigation,
-            history: None,
             on_update: None,
-            initial_route: None,
         }
     }
 }
 
-impl<R: Routable + Clone> RouterConfig<R>
-where
-    <R as std::str::FromStr>::Err: std::fmt::Display,
-{
-    pub(crate) fn take_history(&mut self) -> Box<dyn AnyHistoryProvider> {
-        self.history
-            .take()
-            .unwrap_or_else(|| {
-                let initial_route = self.initial_route.clone().unwrap_or_else(|| "/".parse().unwrap_or_else(|err|
-                    panic!("index route does not exist:\n{}\n use MemoryHistory::with_initial_path or RouterConfig::initial_route to set a custom path", err)
-                ));
-                default_history(initial_route)
-    })
-    }
-}
-
 impl<R> RouterConfig<R>
 where
     R: Routable,
@@ -81,24 +68,6 @@ where
         }
     }
 
-    /// The [`HistoryProvider`] the router should use.
-    ///
-    /// Defaults to a different history provider depending on the target platform.
-    pub fn history(self, history: impl HistoryProvider<R> + 'static) -> Self {
-        Self {
-            history: Some(Box::new(AnyHistoryProviderImplWrapper::new(history))),
-            ..self
-        }
-    }
-
-    /// The initial route the router should use if no history provider is set.
-    pub fn initial_route(self, route: R) -> Self {
-        Self {
-            initial_route: Some(route),
-            ..self
-        }
-    }
-
     /// A component to render when an external navigation fails.
     ///
     /// Defaults to a router-internal component called [`FailureExternalNavigation`]
@@ -109,47 +78,3 @@ where
         }
     }
 }
-
-/// Get the default history provider for the current platform.
-#[allow(unreachable_code, unused)]
-fn default_history<R: Routable + Clone>(initial_route: R) -> Box<dyn AnyHistoryProvider>
-where
-    <R as std::str::FromStr>::Err: std::fmt::Display,
-{
-    // If we're on the web and have wasm, use the web history provider
-
-    #[cfg(all(target_arch = "wasm32", feature = "web"))]
-    return Box::new(AnyHistoryProviderImplWrapper::new(
-        WebHistory::<R>::default(),
-    ));
-
-    // If we're using fullstack and server side rendering, use the memory history provider
-    #[cfg(all(feature = "fullstack", feature = "ssr"))]
-    return Box::new(AnyHistoryProviderImplWrapper::new(
-        MemoryHistory::<R>::with_initial_path(
-            dioxus_fullstack::prelude::server_context()
-                .request_parts()
-                .uri
-                .to_string()
-                .parse()
-                .unwrap_or_else(|err| {
-                    tracing::error!("Failed to parse uri: {}", err);
-                    "/".parse().unwrap_or_else(|err| {
-                        panic!("Failed to parse uri: {}", err);
-                    })
-                }),
-        ),
-    ));
-
-    // If liveview is enabled, use the liveview history provider
-    #[cfg(feature = "liveview")]
-    return Box::new(AnyHistoryProviderImplWrapper::new(
-        LiveviewHistory::new_with_initial_path(initial_route),
-    ));
-
-    // If none of the above, use the memory history provider, which is a decent enough fallback
-    // Eventually we want to integrate with the mobile history provider, and other platform providers
-    Box::new(AnyHistoryProviderImplWrapper::new(
-        MemoryHistory::with_initial_path(initial_route),
-    ))
-}

+ 1 - 13
packages/router/src/utils/use_router_internal.rs

@@ -11,17 +11,5 @@ use crate::prelude::*;
 /// - [`None`], when the current component isn't a descendant of a [`Router`] component.
 /// - Otherwise [`Some`].
 pub(crate) fn use_router_internal() -> Option<RouterContext> {
-    let router = try_consume_context::<RouterContext>()?;
-    let id = current_scope_id().expect("use_router_internal called outside of a component");
-    use_drop({
-        to_owned![router];
-        move || {
-            router.unsubscribe(id);
-        }
-    });
-    use_hook(move || {
-        router.subscribe(id);
-
-        Some(router)
-    })
+    use_hook(try_consume_context)
 }

+ 90 - 4
packages/router/tests/via_ssr/link.rs

@@ -1,13 +1,23 @@
 use dioxus::prelude::*;
-use std::str::FromStr;
+use dioxus_history::{History, MemoryHistory};
+use dioxus_router::components::HistoryProvider;
+use std::{rc::Rc, str::FromStr};
 
 fn prepare<R: Routable>() -> String
+where
+    <R as FromStr>::Err: std::fmt::Display,
+{
+    prepare_at::<R>("/")
+}
+
+fn prepare_at<R: Routable>(at: impl ToString) -> String
 where
     <R as FromStr>::Err: std::fmt::Display,
 {
     let mut vdom = VirtualDom::new_with_props(
         App,
         AppProps::<R> {
+            at: at.to_string(),
             phantom: std::marker::PhantomData,
         },
     );
@@ -16,12 +26,14 @@ where
 
     #[derive(Props)]
     struct AppProps<R: Routable> {
+        at: String,
         phantom: std::marker::PhantomData<R>,
     }
 
     impl<R: Routable> Clone for AppProps<R> {
         fn clone(&self) -> Self {
             Self {
+                at: self.at.clone(),
                 phantom: std::marker::PhantomData,
             }
         }
@@ -34,14 +46,15 @@ where
     }
 
     #[allow(non_snake_case)]
-    fn App<R: Routable>(_props: AppProps<R>) -> Element
+    fn App<R: Routable>(props: AppProps<R>) -> Element
     where
         <R as FromStr>::Err: std::fmt::Display,
     {
         rsx! {
             h1 { "App" }
-            Router::<R> {
-                config: |_| RouterConfig::default().history(MemoryHistory::default())
+            HistoryProvider {
+                history:  move |_| Rc::new(MemoryHistory::with_initial_path(props.at.clone())) as Rc<dyn History>,
+                Router::<R> {}
             }
         }
     }
@@ -345,3 +358,76 @@ fn with_rel() {
 
     assert_eq!(prepare::<Route>(), expected);
 }
+
+#[test]
+fn with_child_route() {
+    #[derive(Routable, Clone, PartialEq, Debug)]
+    enum ChildRoute {
+        #[route("/")]
+        ChildRoot {},
+        #[route("/:not_static")]
+        NotStatic { not_static: String },
+    }
+
+    #[derive(Routable, Clone, PartialEq, Debug)]
+    enum Route {
+        #[route("/")]
+        Root {},
+        #[route("/test")]
+        Test {},
+        #[child("/child")]
+        Nested { child: ChildRoute },
+    }
+
+    #[component]
+    fn Test() -> Element {
+        unimplemented!()
+    }
+
+    #[component]
+    fn Root() -> Element {
+        rsx! {
+            Link {
+                to: Route::Test {},
+                "Parent Link"
+            }
+            Link {
+                to: Route::Nested { child: ChildRoute::NotStatic { not_static: "this-is-a-child-route".to_string() } },
+                "Child Link"
+            }
+        }
+    }
+
+    #[component]
+    fn ChildRoot() -> Element {
+        rsx! {
+            Link {
+                to: Route::Test {},
+                "Parent Link"
+            }
+            Link {
+                to: ChildRoute::NotStatic { not_static: "this-is-a-child-route".to_string() },
+                "Child Link 1"
+            }
+            Link {
+                to: Route::Nested { child: ChildRoute::NotStatic { not_static: "this-is-a-child-route".to_string() } },
+                "Child Link 2"
+            }
+        }
+    }
+
+    #[component]
+    fn NotStatic(not_static: String) -> Element {
+        unimplemented!()
+    }
+
+    assert_eq!(
+        prepare_at::<Route>("/"),
+        "<h1>App</h1><a href=\"/test\">Parent Link</a><a href=\"/child/this-is-a-child-route\">Child Link</a>"
+    );
+
+    assert_eq!(
+        prepare_at::<Route>("/child"),
+        "<h1>App</h1><a href=\"/test\">Parent Link</a><a href=\"/child/this-is-a-child-route\">Child Link 1</a><a href=\"/child/this-is-a-child-route\">Child Link 2</a>"
+    );
+}

+ 7 - 4
packages/router/tests/via_ssr/outlet.rs

@@ -1,6 +1,10 @@
 #![allow(unused)]
 
+use std::rc::Rc;
+
 use dioxus::prelude::*;
+use dioxus_history::{History, MemoryHistory};
+use dioxus_router::components::HistoryProvider;
 use dioxus_router::prelude::*;
 
 fn prepare(path: impl Into<String>) -> VirtualDom {
@@ -38,10 +42,9 @@ fn prepare(path: impl Into<String>) -> VirtualDom {
     fn App(path: Route) -> Element {
         rsx! {
             h1 { "App" }
-            Router::<Route> {
-                config: move |_| {
-                    RouterConfig::default().history(MemoryHistory::with_initial_path(path.clone()))
-                }
+            HistoryProvider {
+                history:  move |_| Rc::new(MemoryHistory::with_initial_path(path.clone())) as Rc<dyn History>,
+                Router::<Route> {}
             }
         }
     }

+ 6 - 5
packages/router/tests/via_ssr/redirect.rs

@@ -1,5 +1,7 @@
 use dioxus::prelude::*;
-use std::str::FromStr;
+use dioxus_history::{History, MemoryHistory};
+use dioxus_router::components::HistoryProvider;
+use std::{rc::Rc, str::FromStr};
 
 // Tests for regressions of <https://github.com/DioxusLabs/dioxus/issues/2549>
 #[test]
@@ -33,10 +35,9 @@ fn Home(lang: String) -> Element {
 #[component]
 fn App(path: Route) -> Element {
     rsx! {
-        Router::<Route> {
-            config: {
-                move |_| RouterConfig::default().history(MemoryHistory::with_initial_path(path.clone()))
-            }
+        HistoryProvider {
+            history:  move |_| Rc::new(MemoryHistory::with_initial_path(path.clone())) as Rc<dyn History>,
+            Router::<Route> {}
         }
     }
 }

+ 7 - 4
packages/router/tests/via_ssr/without_index.rs

@@ -1,4 +1,8 @@
+use std::rc::Rc;
+
 use dioxus::prelude::*;
+use dioxus_history::{History, MemoryHistory};
+use dioxus_router::components::HistoryProvider;
 
 // Tests for regressions of <https://github.com/DioxusLabs/dioxus/issues/2468>
 #[test]
@@ -32,10 +36,9 @@ fn Test() -> Element {
 #[component]
 fn App(path: Route) -> Element {
     rsx! {
-        Router::<Route> {
-            config: {
-                move || RouterConfig::default().history(MemoryHistory::with_initial_path(path))
-            }
+        HistoryProvider {
+            history:  move |_| Rc::new(MemoryHistory::with_initial_path(path)) as Rc<dyn History>,
+            Router::<Route> {}
         }
     }
 }

+ 3 - 3
packages/static-generation/Cargo.toml

@@ -13,7 +13,7 @@ resolver = "2"
 [dependencies]
 dioxus-fullstack = { workspace = true }
 dioxus-lib.workspace = true
-dioxus-router = { workspace = true, features = ["fullstack"]}
+dioxus-router = { workspace = true }
 dioxus-ssr = { workspace = true, optional = true }
 dioxus-isrg = { workspace = true, optional = true }
 axum = { workspace = true, features = ["ws", "macros"], optional = true }
@@ -32,8 +32,8 @@ criterion = { workspace = true }
 
 [features]
 default = []
-server = ["dioxus-fullstack/server", "dioxus-router/ssr", "dep:dioxus-ssr", "dep:tokio", "dep:http", "dep:axum", "dep:tower-http", "dep:dioxus-devtools", "dep:dioxus-cli-config", "dep:tower", "dep:dioxus-isrg"]
-web = ["dioxus-fullstack/web", "dioxus-router/web", "dep:dioxus-web"]
+server = ["dioxus-fullstack/server", "dep:dioxus-ssr", "dep:tokio", "dep:http", "dep:axum", "dep:tower-http", "dep:dioxus-devtools", "dep:dioxus-cli-config", "dep:tower", "dep:dioxus-isrg"]
+web = ["dioxus-fullstack/web", "dep:dioxus-web"]
 
 [package.metadata.docs.rs]
 cargo-args = ["-Zunstable-options", "-Zrustdoc-scrape-examples"]

+ 4 - 0
packages/web/Cargo.toml

@@ -12,7 +12,9 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
 [dependencies]
 dioxus-core = { workspace = true }
 dioxus-core-types = { workspace = true }
+dioxus-cli-config = { workspace = true }
 dioxus-html = { workspace = true }
+dioxus-history = { workspace = true }
 dioxus-document = { workspace = true }
 dioxus-devtools = { workspace = true }
 dioxus-signals = { workspace = true }
@@ -55,6 +57,7 @@ features = [
     "Document",
     "DragEvent",
     "FocusEvent",
+    "History",
     "HtmlElement",
     "HtmlFormElement",
     "HtmlInputElement",
@@ -67,6 +70,7 @@ features = [
     "PointerEvent",
     "ResizeObserverEntry",
     "ResizeObserverSize",
+    "ScrollRestoration",
     "Text",
     "Touch",
     "TouchEvent",

+ 8 - 1
packages/web/src/document.rs

@@ -1,5 +1,6 @@
 use dioxus_core::ScopeId;
 use dioxus_document::{Document, Eval, EvalError, Evaluator};
+use dioxus_history::History;
 use generational_box::{AnyStorage, GenerationalBox, UnsyncStorage};
 use js_sys::Function;
 use serde::Serialize;
@@ -9,6 +10,8 @@ use std::pin::Pin;
 use std::{rc::Rc, str::FromStr};
 use wasm_bindgen::prelude::*;
 
+use crate::history::WebHistory;
+
 #[wasm_bindgen::prelude::wasm_bindgen]
 pub struct JSOwner {
     _owner: Box<dyn std::any::Any>,
@@ -53,12 +56,16 @@ extern "C" {
     pub async fn rust_recv(this: &WeakDioxusChannel) -> wasm_bindgen::JsValue;
 }
 
-/// Provides the WebEvalProvider through [`ScopeId::provide_context`].
+/// Provides the Document through [`ScopeId::provide_context`].
 pub fn init_document() {
     let provider: Rc<dyn Document> = Rc::new(WebDocument);
     if ScopeId::ROOT.has_context::<Rc<dyn Document>>().is_none() {
         ScopeId::ROOT.provide_context(provider);
     }
+    let history_provider: Rc<dyn History> = Rc::new(WebHistory::default());
+    if ScopeId::ROOT.has_context::<Rc<dyn History>>().is_none() {
+        ScopeId::ROOT.provide_context(history_provider);
+    }
 }
 
 /// The web-target's document provider.

+ 97 - 79
packages/router/src/history/web.rs → packages/web/src/history/mod.rs

@@ -1,35 +1,24 @@
-use std::{
-    path::PathBuf,
-    sync::{Arc, Mutex},
-};
+use scroll::ScrollPosition;
+use std::path::PathBuf;
+use wasm_bindgen::JsCast;
+use wasm_bindgen::{prelude::Closure, JsValue};
+use web_sys::{window, Window};
+use web_sys::{Event, History, ScrollRestoration};
 
-use gloo::{console::error, events::EventListener, render::AnimationFrame};
-
-use wasm_bindgen::JsValue;
-use web_sys::{window, History, ScrollRestoration, Window};
-
-use crate::routable::Routable;
-
-use super::{
-    web_history::{get_current, push_state_and_url, replace_state_with_url},
-    web_scroll::ScrollPosition,
-    HistoryProvider,
-};
+mod scroll;
 
 #[allow(dead_code)]
 fn base_path() -> Option<PathBuf> {
-    tracing::trace!(
-        "Using base_path from the CLI: {:?}",
-        dioxus_cli_config::base_path()
-    );
-    dioxus_cli_config::base_path()
+    let base_path = dioxus_cli_config::base_path();
+    tracing::trace!("Using base_path from the CLI: {:?}", base_path);
+    base_path
 }
 
 #[allow(clippy::extra_unused_type_parameters)]
-fn update_scroll<R>(window: &Window, history: &History) {
+fn update_scroll(window: &Window, history: &History) {
     let scroll = ScrollPosition::of_window(window);
     if let Err(err) = replace_state_with_url(history, &[scroll.x, scroll.y], None) {
-        error!(err);
+        web_sys::console::error_1(&err);
     }
 }
 
@@ -45,50 +34,38 @@ fn update_scroll<R>(window: &Window, history: &History) {
 ///
 /// Application developers are responsible for not rendering the router if the prefix is not present
 /// in the URL. Otherwise, if a router navigation is triggered, the prefix will be added.
-pub struct WebHistory<R: Routable> {
+pub struct WebHistory {
     do_scroll_restoration: bool,
     history: History,
-    listener_navigation: Option<EventListener>,
-    listener_animation_frame: Arc<Mutex<Option<AnimationFrame>>>,
     prefix: Option<String>,
     window: Window,
-    phantom: std::marker::PhantomData<R>,
 }
 
-impl<R: Routable> Default for WebHistory<R>
-where
-    <R as std::str::FromStr>::Err: std::fmt::Display,
-{
+impl Default for WebHistory {
     fn default() -> Self {
         Self::new(None, true)
     }
 }
 
-impl<R: Routable> WebHistory<R> {
+impl WebHistory {
     /// Create a new [`WebHistory`].
     ///
     /// If `do_scroll_restoration` is [`true`], [`WebHistory`] will take control of the history
     /// state. It'll also set the browsers scroll restoration to `manual`.
-    pub fn new(prefix: Option<String>, do_scroll_restoration: bool) -> Self
-    where
-        <R as std::str::FromStr>::Err: std::fmt::Display,
-    {
+    pub fn new(prefix: Option<String>, do_scroll_restoration: bool) -> Self {
         let myself = Self::new_inner(prefix, do_scroll_restoration);
 
-        let current_route = myself.current_route();
+        let current_route = dioxus_history::History::current_route(&myself);
         let current_route_str = current_route.to_string();
         let prefix_str = myself.prefix.as_deref().unwrap_or("");
         let current_url = format!("{prefix_str}{current_route_str}");
-        let state = myself.create_state(current_route);
+        let state = myself.create_state();
         let _ = replace_state_with_url(&myself.history, &state, Some(&current_url));
 
         myself
     }
 
-    fn new_inner(prefix: Option<String>, do_scroll_restoration: bool) -> Self
-    where
-        <R as std::str::FromStr>::Err: std::fmt::Display,
-    {
+    fn new_inner(prefix: Option<String>, do_scroll_restoration: bool) -> Self {
         let window = window().expect("access to `window`");
         let history = window.history().expect("`window` has access to `history`");
 
@@ -111,11 +88,8 @@ impl<R: Routable> WebHistory<R> {
         Self {
             do_scroll_restoration,
             history,
-            listener_navigation: None,
-            listener_animation_frame: Default::default(),
             prefix,
             window,
-            phantom: Default::default(),
         }
     }
 
@@ -125,17 +99,14 @@ impl<R: Routable> WebHistory<R> {
             .unwrap_or_default()
     }
 
-    fn create_state(&self, _state: R) -> [f64; 2] {
+    fn create_state(&self) -> [f64; 2] {
         let scroll = self.scroll_pos();
         [scroll.x, scroll.y]
     }
 }
 
-impl<R: Routable> WebHistory<R>
-where
-    <R as std::str::FromStr>::Err: std::fmt::Display,
-{
-    fn route_from_location(&self) -> R {
+impl WebHistory {
+    fn route_from_location(&self) -> String {
         let location = self.window.location();
         let path = location.pathname().unwrap_or_else(|_| "/".into())
             + &location.search().unwrap_or("".into())
@@ -148,12 +119,12 @@ where
         if path.is_empty() {
             path = "/"
         }
-        R::from_str(path).unwrap_or_else(|err| panic!("{}", err))
+        path.to_string()
     }
 
-    fn full_path(&self, state: &R) -> String {
+    fn full_path(&self, state: &String) -> String {
         match &self.prefix {
-            None => format!("{state}"),
+            None => state.to_string(),
             Some(prefix) => format!("{prefix}{state}"),
         }
     }
@@ -165,26 +136,30 @@ where
                     self.window.scroll_to_with_x_and_y(0.0, 0.0)
                 }
             }
-            Err(e) => error!("failed to change state: ", e),
+            Err(e) => {
+                web_sys::console::error_2(&JsValue::from_str("failed to change state: "), &e);
+            }
         }
     }
 
-    fn navigate_external(&mut self, url: String) -> bool {
+    fn navigate_external(&self, url: String) -> bool {
         match self.window.location().set_href(&url) {
             Ok(_) => true,
             Err(e) => {
-                error!("failed to navigate to external url (", url, "): ", e);
+                web_sys::console::error_4(
+                    &JsValue::from_str("failed to navigate to external url ("),
+                    &JsValue::from_str(&url),
+                    &JsValue::from_str("): "),
+                    &e,
+                );
                 false
             }
         }
     }
 }
 
-impl<R: Routable> HistoryProvider<R> for WebHistory<R>
-where
-    <R as std::str::FromStr>::Err: std::fmt::Display,
-{
-    fn current_route(&self) -> R {
+impl dioxus_history::History for WebHistory {
+    fn current_route(&self) -> String {
         self.route_from_location()
     }
 
@@ -192,20 +167,20 @@ where
         self.prefix.clone()
     }
 
-    fn go_back(&mut self) {
+    fn go_back(&self) {
         if let Err(e) = self.history.back() {
-            error!("failed to go back: ", e)
+            web_sys::console::error_2(&JsValue::from_str("failed to go back: "), &e);
         }
     }
 
-    fn go_forward(&mut self) {
+    fn go_forward(&self) {
         if let Err(e) = self.history.forward() {
-            error!("failed to go forward: ", e)
+            web_sys::console::error_2(&JsValue::from_str("failed to go forward: "), &e);
         }
     }
 
-    fn push(&mut self, state: R) {
-        if state.to_string() == self.current_route().to_string() {
+    fn push(&self, state: String) {
+        if state == self.current_route() {
             // don't push the same state twice
             return;
         }
@@ -214,39 +189,82 @@ where
         let h = w.history().expect("`window` has access to `history`");
 
         // update the scroll position before pushing the new state
-        update_scroll::<R>(&w, &h);
+        update_scroll(&w, &h);
 
         let path = self.full_path(&state);
 
-        let state: [f64; 2] = self.create_state(state);
+        let state: [f64; 2] = self.create_state();
         self.handle_nav(push_state_and_url(&self.history, &state, path));
     }
 
-    fn replace(&mut self, state: R) {
+    fn replace(&self, state: String) {
         let path = self.full_path(&state);
 
-        let state = self.create_state(state);
+        let state = self.create_state();
         self.handle_nav(replace_state_with_url(&self.history, &state, Some(&path)));
     }
 
-    fn external(&mut self, url: String) -> bool {
+    fn external(&self, url: String) -> bool {
         self.navigate_external(url)
     }
 
-    fn updater(&mut self, callback: std::sync::Arc<dyn Fn() + Send + Sync>) {
+    fn updater(&self, callback: std::sync::Arc<dyn Fn() + Send + Sync>) {
         let w = self.window.clone();
         let h = self.history.clone();
-        let s = self.listener_animation_frame.clone();
         let d = self.do_scroll_restoration;
 
-        self.listener_navigation = Some(EventListener::new(&self.window, "popstate", move |_| {
+        let function = Closure::wrap(Box::new(move |_| {
             (*callback)();
             if d {
-                let mut s = s.lock().expect("unpoisoned scroll mutex");
                 if let Some([x, y]) = get_current(&h) {
-                    *s = Some(ScrollPosition { x, y }.scroll_to(w.clone()));
+                    ScrollPosition { x, y }.scroll_to(w.clone())
                 }
             }
-        }));
+        }) as Box<dyn FnMut(Event)>);
+        self.window
+            .add_event_listener_with_callback(
+                "popstate",
+                &function.into_js_value().unchecked_into(),
+            )
+            .unwrap();
+    }
+}
+
+pub(crate) fn replace_state_with_url(
+    history: &History,
+    value: &[f64; 2],
+    url: Option<&str>,
+) -> Result<(), JsValue> {
+    let position = js_sys::Array::new();
+    position.push(&JsValue::from(value[0]));
+    position.push(&JsValue::from(value[1]));
+
+    history.replace_state_with_url(&position, "", url)
+}
+
+pub(crate) fn push_state_and_url(
+    history: &History,
+    value: &[f64; 2],
+    url: String,
+) -> Result<(), JsValue> {
+    let position = js_sys::Array::new();
+    position.push(&JsValue::from(value[0]));
+    position.push(&JsValue::from(value[1]));
+
+    history.push_state_with_url(&position, "", Some(&url))
+}
+
+pub(crate) fn get_current(history: &History) -> Option<[f64; 2]> {
+    use wasm_bindgen::JsCast;
+
+    let state = history.state();
+    if let Err(err) = &state {
+        web_sys::console::error_1(err);
     }
+    state.ok().and_then(|state| {
+        let state = state.dyn_into::<js_sys::Array>().ok()?;
+        let x = state.get(0).as_f64()?;
+        let y = state.get(1).as_f64()?;
+        Some([x, y])
+    })
 }

+ 28 - 0
packages/web/src/history/scroll.rs

@@ -0,0 +1,28 @@
+use wasm_bindgen::{prelude::Closure, JsCast};
+use web_sys::Window;
+
+#[derive(Clone, Copy, Debug, Default)]
+pub(crate) struct ScrollPosition {
+    pub x: f64,
+    pub y: f64,
+}
+
+impl ScrollPosition {
+    pub(crate) fn of_window(window: &Window) -> Self {
+        Self {
+            x: window.scroll_x().unwrap_or_default(),
+            y: window.scroll_y().unwrap_or_default(),
+        }
+    }
+
+    pub(crate) fn scroll_to(&self, window: Window) {
+        let Self { x, y } = *self;
+        let f = Closure::wrap(
+            Box::new(move || window.scroll_to_with_x_and_y(x, y)) as Box<dyn FnMut()>
+        );
+        web_sys::window()
+            .expect("should be run in a context with a `Window` object (dioxus cannot be run from a web worker)")
+            .request_animation_frame(&f.into_js_value().unchecked_into())
+            .expect("should register `requestAnimationFrame` OK");
+    }
+}

+ 2 - 0
packages/web/src/lib.rs

@@ -39,6 +39,8 @@ mod document;
 #[cfg(feature = "file_engine")]
 mod file_engine;
 #[cfg(feature = "document")]
+mod history;
+#[cfg(feature = "document")]
 pub use document::WebDocument;
 #[cfg(feature = "file_engine")]
 pub use file_engine::*;