dioxus_fullstack_core/
history.rs

1//! A history provider for fullstack apps that is compatible with hydration.
2
3use std::{cell::RefCell, rc::Rc};
4
5use crate::transport::{is_hydrating, SerializeContextEntry};
6use dioxus_core::{provide_context, queue_effect, schedule_update, try_consume_context};
7use dioxus_history::{history, provide_history_context, History};
8
9// If we are currently in a scope and this is the first run then queue a rerender
10// for after hydration
11fn match_hydration<O>(
12    during_hydration: impl FnOnce() -> O,
13    after_hydration: impl FnOnce() -> O,
14) -> O {
15    if is_hydrating() {
16        let update = schedule_update();
17        queue_effect(move || update());
18        during_hydration()
19    } else {
20        after_hydration()
21    }
22}
23
24#[derive(Debug, Clone, PartialEq)]
25pub(crate) struct ResolvedRouteContext {
26    route: String,
27}
28
29pub(crate) fn finalize_route() {
30    // This may run in tests without the full hydration context set up, if it does, then just
31    // return without modifying the context
32    let Some(entry) = try_consume_context::<RouteEntry>() else {
33        return;
34    };
35
36    let Some(entry) = entry.entry.borrow_mut().take() else {
37        // If it was already taken, then just return. This can happen if commit_initial_chunk is called twice
38        return;
39    };
40
41    if cfg!(feature = "server") {
42        let history = history();
43        let route = history.current_route();
44        entry.insert(&route, std::panic::Location::caller());
45        provide_context(ResolvedRouteContext { route });
46    } else if cfg!(feature = "web") {
47        let route = entry
48            .get()
49            .expect("Failed to get initial route from hydration context");
50        provide_context(ResolvedRouteContext { route });
51    }
52}
53
54/// Provide the fullstack history context. This interacts with the hydration context so it must
55/// be called in the same order on the client and server after the hydration context is created
56pub fn provide_fullstack_history_context<H: History + 'static>(history: H) {
57    let entry = crate::transport::serialize_context().create_entry();
58    provide_context(RouteEntry {
59        entry: Rc::new(RefCell::new(Some(entry.clone()))),
60    });
61    provide_history_context(Rc::new(FullstackHistory::new(history)));
62}
63
64#[derive(Clone)]
65struct RouteEntry {
66    entry: Rc<RefCell<Option<SerializeContextEntry<String>>>>,
67}
68
69/// A history provider for fullstack apps that is compatible with hydration.
70#[derive(Clone)]
71struct FullstackHistory<H> {
72    history: H,
73}
74
75impl<H> FullstackHistory<H> {
76    /// Create a new `FullstackHistory` with the given history.
77    pub fn new(history: H) -> Self {
78        Self { history }
79    }
80
81    /// Get the initial route of the history.
82    fn initial_route(&self) -> String
83    where
84        H: History,
85    {
86        // If the route hydration entry is set, use that instead of the histories current route
87        // for better hydration behavior. The client may be rendering from a ssg route that was
88        // rendered at a different url
89        if let Some(entry) = try_consume_context::<RouteEntry>() {
90            let entry = entry.entry.borrow();
91            if let Some(entry) = &*entry {
92                if let Ok(initial_route) = entry.get() {
93                    return initial_route;
94                }
95            }
96        }
97
98        self.history.current_route()
99    }
100}
101
102impl<H: History> History for FullstackHistory<H> {
103    fn current_prefix(&self) -> Option<String> {
104        self.history.current_prefix()
105    }
106
107    fn can_go_back(&self) -> bool {
108        match_hydration(|| false, || self.history.can_go_back())
109    }
110
111    fn can_go_forward(&self) -> bool {
112        match_hydration(|| false, || self.history.can_go_forward())
113    }
114
115    fn external(&self, url: String) -> bool {
116        self.history.external(url)
117    }
118
119    fn updater(&self, callback: std::sync::Arc<dyn Fn() + Send + Sync>) {
120        self.history.updater(callback)
121    }
122
123    fn include_prevent_default(&self) -> bool {
124        self.history.include_prevent_default()
125    }
126
127    fn current_route(&self) -> String {
128        match_hydration(|| self.initial_route(), || self.history.current_route())
129    }
130
131    fn go_back(&self) {
132        self.history.go_back();
133    }
134
135    fn go_forward(&self) {
136        self.history.go_forward();
137    }
138
139    fn push(&self, route: String) {
140        self.history.push(route);
141    }
142
143    fn replace(&self, path: String) {
144        self.history.replace(path);
145    }
146}