1use dioxus::history::History;
4use std::rc::Rc;
5
6pub fn transition_history(inherited: Rc<dyn History>) -> Rc<dyn History> {
7 #[cfg(target_arch = "wasm32")]
8 if let Some(history) = browser::TransitionHistory::connect(inherited.current_prefix()) {
9 return Rc::new(history);
10 }
11 inherited
12}
13
14#[cfg(target_arch = "wasm32")]
15mod browser {
16 use super::*;
17 use crate::bridges::navigation::wasm::{get_module, NavigationModule};
18 use std::{cell::RefCell, sync::Arc};
19 use wasm_bindgen::prelude::Closure;
20
21 type Updater = Arc<dyn Fn() + Send + Sync>;
22
23 pub struct TransitionHistory {
24 module: NavigationModule,
25 updater: Rc<RefCell<Option<Updater>>>,
26 _notify: Closure<dyn FnMut()>,
28 }
29
30 impl TransitionHistory {
31 pub fn connect(prefix: Option<String>) -> Option<Self> {
32 let module = get_module()?;
33 let updater = Rc::new(RefCell::new(None::<Updater>));
34 let notify = {
35 let updater = updater.clone();
36 Closure::wrap(Box::new(move || {
37 let callback = updater.borrow().clone();
38 if let Some(callback) = callback {
39 callback();
40 }
41 }) as Box<dyn FnMut()>)
42 };
43 if module.connect(¬ify, prefix.as_deref()).is_err() {
44 let _ = module.disconnect();
45 return None;
46 }
47 Some(Self {
48 module,
49 updater,
50 _notify: notify,
51 })
52 }
53 }
54
55 impl History for TransitionHistory {
56 fn current_route(&self) -> String {
57 self.module.current_route()
59 }
60
61 fn current_prefix(&self) -> Option<String> {
62 self.module.current_prefix()
63 }
64
65 fn go_back(&self) {
66 self.module.back();
67 }
68
69 fn go_forward(&self) {
70 self.module.forward();
71 }
72
73 fn push(&self, route: String) {
74 self.module.push(&route);
75 }
76
77 fn replace(&self, route: String) {
78 self.module.replace(&route);
79 }
80
81 fn external(&self, url: String) -> bool {
82 self.module.external(&url)
83 }
84
85 fn updater(&self, callback: Updater) {
86 *self.updater.borrow_mut() = Some(callback);
88 }
89 }
90
91 impl Drop for TransitionHistory {
92 fn drop(&mut self) {
93 let _ = self.module.disconnect();
94 }
95 }
96}
97
98#[cfg(all(test, not(target_arch = "wasm32")))]
99mod tests {
100 use super::*;
101 use dioxus::prelude::*;
102 use dioxus::router::components::HistoryProvider;
103 use std::{cell::RefCell, sync::Arc, time::Duration};
104
105 #[test]
106 fn native_history_retains_server_route_and_history_identity() {
107 let inherited: Rc<dyn History> = Rc::new(
108 dioxus::history::MemoryHistory::with_initial_path("/post/server-route".to_owned()),
109 );
110 let history = transition_history(inherited.clone());
111 assert!(Rc::ptr_eq(&history, &inherited));
112 assert_eq!(history.current_route(), "/post/server-route");
113 history.push("/archives".to_owned());
114 history.go_back();
115 assert_eq!(history.current_route(), "/post/server-route");
116 }
117
118 struct DeferredHistory {
120 displayed: RefCell<String>,
121 pending: RefCell<Option<String>>,
122 notify: RefCell<Option<Arc<dyn Fn() + Send + Sync>>>,
123 }
124
125 impl History for DeferredHistory {
126 fn current_route(&self) -> String {
127 self.displayed.borrow().clone()
128 }
129
130 fn push(&self, route: String) {
131 *self.pending.borrow_mut() = Some(route);
132 }
133
134 fn replace(&self, route: String) {
135 self.push(route);
136 }
137
138 fn go_back(&self) {}
139 fn go_forward(&self) {}
140
141 fn updater(&self, callback: Arc<dyn Fn() + Send + Sync>) {
142 *self.notify.borrow_mut() = Some(callback);
143 }
144 }
145
146 #[derive(Clone)]
147 struct ProbeState {
148 history: Rc<DeferredHistory>,
149 resources: Rc<RefCell<Vec<String>>>,
150 effects: Rc<RefCell<Vec<String>>>,
151 scope: Rc<RefCell<Option<ScopeId>>>,
152 }
153
154 #[derive(Clone, Routable, PartialEq)]
155 enum ProbeRoute {
156 #[route("/preview/:slug")]
157 Probe { slug: String },
158 }
159
160 #[component]
161 fn Probe(slug: String) -> Element {
162 let state: ProbeState = use_context();
163 use_hook(|| *state.scope.borrow_mut() = Some(dioxus::dioxus_core::current_scope_id()));
164 let router = dioxus::router::router();
165 let committed_slug = use_memo(move || {
166 let ProbeRoute::Probe { slug } = router.current::<ProbeRoute>();
167 slug
168 });
169 let _request = use_resource(move || {
170 let slug = committed_slug();
171 state.resources.borrow_mut().push(slug.clone());
172 async move { slug }
173 });
174 use_effect(move || state.effects.borrow_mut().push(committed_slug()));
175 rsx! { div { "{slug}" } }
176 }
177
178 fn probe_app(state: ProbeState) -> Element {
179 use_context_provider(|| state.clone());
180 rsx! {
181 HistoryProvider {
182 history: move |_| state.history.clone() as Rc<dyn History>,
183 Router::<ProbeRoute> {}
184 }
185 }
186 }
187
188 async fn settle(dom: &mut VirtualDom) {
189 for _ in 0..10 {
190 dom.render_immediate(&mut dioxus::dioxus_core::NoOpMutations);
191 tokio::select! {
192 biased;
193 _ = dom.wait_for_work() => {},
194 _ = tokio::time::sleep(Duration::from_millis(5)) => return,
195 }
196 }
197 panic!("navigation did not settle");
198 }
199
200 #[tokio::test]
201 async fn deferred_router_updates_only_reload_data_when_the_displayed_slug_changes() {
202 let state = ProbeState {
203 history: Rc::new(DeferredHistory {
204 displayed: RefCell::new("/preview/first".to_owned()),
205 pending: RefCell::new(None),
206 notify: RefCell::new(None),
207 }),
208 resources: Rc::new(RefCell::new(Vec::new())),
209 effects: Rc::new(RefCell::new(Vec::new())),
210 scope: Rc::new(RefCell::new(None)),
211 };
212 let mut dom = VirtualDom::new_with_props(probe_app, state.clone());
213 dom.rebuild_in_place();
214 settle(&mut dom).await;
215 assert_eq!(*state.resources.borrow(), ["first"]);
216 assert_eq!(*state.effects.borrow(), ["first"]);
217
218 dom.in_scope(state.scope.borrow().unwrap(), || {
220 dioxus::router::navigator().push(ProbeRoute::Probe {
221 slug: "second".to_owned(),
222 });
223 });
224 settle(&mut dom).await;
225 assert_eq!(state.history.current_route(), "/preview/first");
226 assert_eq!(*state.resources.borrow(), ["first"]);
227 assert_eq!(*state.effects.borrow(), ["first"]);
228
229 *state.history.displayed.borrow_mut() = state.history.pending.borrow_mut().take().unwrap();
231 state.history.notify.borrow().as_ref().unwrap()();
232 settle(&mut dom).await;
233 assert_eq!(*state.resources.borrow(), ["first", "second"]);
234 assert_eq!(*state.effects.borrow(), ["first", "second"]);
235 }
236}