From 315854b4492e302e3be7a39158cf5e5fe1049974 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 18 Jun 2025 18:48:59 -0500 Subject: [PATCH] fix can go back hydration (#4209) --- Cargo.lock | 1 + packages/dioxus/src/launch.rs | 16 ++- packages/fullstack-hooks/Cargo.toml | 1 + packages/fullstack-hooks/src/history.rs | 118 ++++++++++++++++++ packages/fullstack-hooks/src/lib.rs | 10 +- packages/fullstack/src/lib.rs | 4 +- .../fullstack-routing/src/main.rs | 39 ++++++ packages/router/src/components/router.rs | 2 +- packages/server/src/render.rs | 5 +- packages/web/src/lib.rs | 2 + 10 files changed, 190 insertions(+), 8 deletions(-) create mode 100644 packages/fullstack-hooks/src/history.rs diff --git a/Cargo.lock b/Cargo.lock index 8e9e43e69..d0a3863d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4115,6 +4115,7 @@ dependencies = [ "dioxus-core", "dioxus-fullstack", "dioxus-fullstack-protocol", + "dioxus-history", "dioxus-hooks", "dioxus-lib", "dioxus-signals", diff --git a/packages/dioxus/src/launch.rs b/packages/dioxus/src/launch.rs index 87d755109..a94cbd43b 100644 --- a/packages/dioxus/src/launch.rs +++ b/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; + 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); } diff --git a/packages/fullstack-hooks/Cargo.toml b/packages/fullstack-hooks/Cargo.toml index 21b3dadc9..a825f9194 100644 --- a/packages/fullstack-hooks/Cargo.toml +++ b/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 } diff --git a/packages/fullstack-hooks/src/history.rs b/packages/fullstack-hooks/src/history.rs new file mode 100644 index 000000000..1feb17416 --- /dev/null +++ b/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( + 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 { + initial_route: OnceCell, + #[cfg(feature = "server")] + in_hydration_context: std::cell::Cell, + history: H, +} + +impl FullstackHistory { + /// 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 History for FullstackHistory { + fn current_prefix(&self) -> Option { + 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) { + 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); + } +} diff --git a/packages/fullstack-hooks/src/lib.rs b/packages/fullstack-hooks/src/lib.rs index e83a83ebf..56014bcac 100644 --- a/packages/fullstack-hooks/src/lib.rs +++ b/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::*; +} diff --git a/packages/fullstack/src/lib.rs b/packages/fullstack/src/lib.rs index 05a2c6aa1..5726dd91c 100644 --- a/packages/fullstack/src/lib.rs +++ b/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}; diff --git a/packages/playwright-tests/fullstack-routing/src/main.rs b/packages/playwright-tests/fullstack-routing/src/main.rs index b7f56f11f..c0bba2175 100644 --- a/packages/playwright-tests/fullstack-routing/src/main.rs +++ b/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}" + }, + } + } + }, + } +} diff --git a/packages/router/src/components/router.rs b/packages/router/src/components/router.rs index 1ea2ee54a..2616d5c43 100644 --- a/packages/router/src/components/router.rs +++ b/packages/router/src/components/router.rs @@ -46,7 +46,7 @@ pub fn Router(props: RouterProps) -> Element { #[cfg(feature = "streaming")] use_after_suspense_resolved(|| { - dioxus_fullstack_hooks::commit_initial_chunk(); + dioxus_fullstack_hooks::prelude::commit_initial_chunk(); }); use_hook(|| { diff --git a/packages/server/src/render.rs b/packages/server/src/render.rs index f69f722e4..21f687221 100644 --- a/packages/server/src/render.rs +++ b/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); diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 6d7a9de9e..34c211a36 100644 --- a/packages/web/src/lib.rs +++ b/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;