Browse Source

fix can go back hydration (#4209)

Evan Almloff 1 week ago
parent
commit
315854b449

+ 1 - 0
Cargo.lock

@@ -4115,6 +4115,7 @@ dependencies = [
  "dioxus-core",
  "dioxus-fullstack",
  "dioxus-fullstack-protocol",
+ "dioxus-history",
  "dioxus-hooks",
  "dioxus-lib",
  "dioxus-signals",

+ 13 - 3
packages/dioxus/src/launch.rs

@@ -357,9 +357,6 @@ impl LaunchBuilder {
                     .hydrate(true);
 
                 let mut vdom = dioxus_core::VirtualDom::new(app);
-                for context in contexts {
-                    vdom.insert_any_root_context(context());
-                }
 
                 #[cfg(feature = "document")]
                 {
@@ -369,6 +366,19 @@ impl LaunchBuilder {
                     vdom.provide_root_context(document);
                 }
 
+                #[cfg(feature = "document")]
+                {
+                    use dioxus_fullstack::FullstackHistory;
+                    let history =
+                        std::rc::Rc::new(FullstackHistory::new(dioxus_web::WebHistory::default()))
+                            as std::rc::Rc<dyn crate::prelude::History>;
+                    vdom.provide_root_context(history);
+                }
+
+                for context in contexts {
+                    vdom.insert_any_root_context(context());
+                }
+
                 return dioxus_web::launch::launch_virtual_dom(vdom, platform_config);
             }
 

+ 1 - 0
packages/fullstack-hooks/Cargo.toml

@@ -17,6 +17,7 @@ dioxus-hooks = { workspace = true }
 dioxus-fullstack-protocol = { workspace = true }
 futures-channel = { workspace = true }
 serde = { workspace = true }
+dioxus-history.workspace = true
 
 [dev-dependencies]
 dioxus-fullstack = { workspace = true }

+ 118 - 0
packages/fullstack-hooks/src/history.rs

@@ -0,0 +1,118 @@
+//! A history provider for fullstack apps that is compatible with hydration.
+
+use std::cell::OnceCell;
+
+use dioxus_core::{
+    prelude::{generation, queue_effect},
+    schedule_update,
+};
+use dioxus_history::History;
+
+// If we are currently in a scope and this is the first run then queue a rerender
+// for after hydration
+fn match_hydration<O>(
+    during_hydration: impl FnOnce() -> O,
+    after_hydration: impl FnOnce() -> O,
+) -> O {
+    if generation() == 0 {
+        let update = schedule_update();
+        queue_effect(move || update());
+        during_hydration()
+    } else {
+        after_hydration()
+    }
+}
+
+/// A history provider for fullstack apps that is compatible with hydration.
+#[derive(Clone)]
+pub struct FullstackHistory<H> {
+    initial_route: OnceCell<String>,
+    #[cfg(feature = "server")]
+    in_hydration_context: std::cell::Cell<bool>,
+    history: H,
+}
+
+impl<H> FullstackHistory<H> {
+    /// Create a new `FullstackHistory` with the given history.
+    pub fn new(history: H) -> Self {
+        Self {
+            initial_route: OnceCell::new(),
+            #[cfg(feature = "server")]
+            in_hydration_context: std::cell::Cell::new(false),
+            history,
+        }
+    }
+
+    /// Create a new `FullstackHistory` with the given history and initial route.
+    pub fn new_server(history: H) -> Self
+    where
+        H: History,
+    {
+        let initial_route = history.current_route();
+        let history = Self::new(history);
+        history.initial_route.set(initial_route).unwrap();
+        history
+    }
+
+    /// Get the initial route of the history.
+    fn initial_route(&self) -> String {
+        let entry = dioxus_fullstack_protocol::serialize_context().create_entry();
+        let route = self.initial_route.get_or_init(|| {
+            entry
+                .get()
+                .expect("Failed to get initial route from hydration context")
+        });
+        #[cfg(feature = "server")]
+        if !self.in_hydration_context.get() {
+            entry.insert(route, std::panic::Location::caller());
+            self.in_hydration_context.set(true);
+        }
+        route.clone()
+    }
+}
+
+impl<H: History> History for FullstackHistory<H> {
+    fn current_prefix(&self) -> Option<String> {
+        self.history.current_prefix()
+    }
+
+    fn can_go_back(&self) -> bool {
+        match_hydration(|| false, || self.history.can_go_back())
+    }
+
+    fn can_go_forward(&self) -> bool {
+        match_hydration(|| false, || self.history.can_go_forward())
+    }
+
+    fn external(&self, url: String) -> bool {
+        self.history.external(url)
+    }
+
+    fn updater(&self, callback: std::sync::Arc<dyn Fn() + Send + Sync>) {
+        self.history.updater(callback)
+    }
+
+    fn include_prevent_default(&self) -> bool {
+        self.history.include_prevent_default()
+    }
+
+    fn current_route(&self) -> String {
+        match_hydration(|| self.initial_route(), || self.history.current_route())
+    }
+
+    fn go_back(&self) {
+        self.history.go_back();
+    }
+
+    fn go_forward(&self) {
+        self.history.go_forward();
+    }
+
+    fn push(&self, route: String) {
+        self.history.push(route);
+    }
+
+    fn replace(&self, path: String) {
+        self.history.replace(path);
+    }
+}

+ 8 - 2
packages/fullstack-hooks/src/lib.rs

@@ -1,7 +1,13 @@
 #![warn(missing_docs)]
 #![doc = include_str!("../README.md")]
 
+pub mod history;
 mod hooks;
-pub use hooks::*;
 mod streaming;
-pub use streaming::*;
+
+pub mod prelude {
+    //! A prelude of commonly used items in dioxus-fullstack-hooks.
+
+    pub use crate::hooks::*;
+    pub use crate::streaming::*;
+}

+ 3 - 1
packages/fullstack/src/lib.rs

@@ -13,9 +13,11 @@ pub use web::FullstackWebDocument;
 #[cfg(feature = "server")]
 pub use dioxus_server::*;
 
+pub use dioxus_fullstack_hooks::history::FullstackHistory;
+
 /// A prelude of commonly used items in dioxus-fullstack.
 pub mod prelude {
-    pub use dioxus_fullstack_hooks::*;
+    pub use dioxus_fullstack_hooks::prelude::*;
 
     pub use dioxus_server_macro::*;
     pub use server_fn::{self, ServerFn as _, ServerFnError};

+ 39 - 0
packages/playwright-tests/fullstack-routing/src/main.rs

@@ -29,6 +29,9 @@ enum Route {
 
     #[route("/error")]
     ThrowsError,
+
+    #[route("/can-go-back")]
+    HydrateCanGoBack,
 }
 
 #[component]
@@ -52,3 +55,39 @@ fn Home() -> Element {
         "Home"
     }
 }
+
+#[component]
+pub fn HydrateCanGoBack() -> Element {
+    let navigator = use_navigator();
+    let mut count = use_signal(|| 0);
+    rsx! {
+        header {
+            class:"flex justify-start items-center app-bg-color-primary px-5 py-2 space-x-4",
+            if navigator.can_go_back() {
+                button  {
+                    class: "app-button-circle item-navbar",
+                    onclick: move |_| {
+                        count += 1;
+                    },
+                    "{count}"
+                },
+            }
+            else {
+                div {
+                    Link  {
+                        class: "app-button-circle item-navbar",
+                        to: Route::Home,
+                        "Go to home"
+                    },
+                    button  {
+                        class: "app-button-circle item-navbar",
+                        onclick: move |_| {
+                            count += 1;
+                        },
+                        "{count}"
+                    },
+                }
+            }
+        },
+    }
+}

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

@@ -46,7 +46,7 @@ pub fn Router<R: Routable + Clone>(props: RouterProps<R>) -> Element {
 
     #[cfg(feature = "streaming")]
     use_after_suspense_resolved(|| {
-        dioxus_fullstack_hooks::commit_initial_chunk();
+        dioxus_fullstack_hooks::prelude::commit_initial_chunk();
     });
 
     use_hook(|| {

+ 4 - 1
packages/server/src/render.rs

@@ -5,7 +5,8 @@ use crate::{
     DioxusServerContext,
 };
 use dioxus_cli_config::base_path;
-use dioxus_fullstack_hooks::{StreamingContext, StreamingStatus};
+use dioxus_fullstack_hooks::history::FullstackHistory;
+use dioxus_fullstack_hooks::prelude::{StreamingContext, StreamingStatus};
 use dioxus_fullstack_protocol::{HydrationContext, SerializedHydrationData};
 use dioxus_isrg::{CachedRender, IncrementalRendererError, RenderFreshness};
 use dioxus_lib::document::Document;
@@ -200,6 +201,8 @@ impl SsrRendererPool {
             } else {
                 history = dioxus_history::MemoryHistory::with_initial_path(&route);
             }
+            // Wrap the memory history in a fullstack history provider to provide the initial route for hydration
+            let history = FullstackHistory::new_server(history);
 
             let streaming_context = in_root_scope(&virtual_dom, StreamingContext::new);
             virtual_dom.provide_root_context(Rc::new(history) as Rc<dyn dioxus_history::History>);

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

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