dioxus_web/
lib.rs

1#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
2#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
3#![deny(missing_docs)]
4
5//! # Dioxus Web
6
7pub use crate::cfg::Config;
8use crate::hydration::SuspenseMessage;
9use dioxus_core::{ScopeId, VirtualDom};
10use dom::WebsysDom;
11use futures_util::{pin_mut, select, FutureExt, StreamExt};
12
13mod cfg;
14mod dom;
15
16mod events;
17pub mod launch;
18mod mutations;
19pub use events::*;
20
21#[cfg(feature = "document")]
22mod document;
23#[cfg(feature = "document")]
24mod history;
25#[cfg(feature = "document")]
26pub use document::WebDocument;
27#[cfg(feature = "document")]
28pub use history::{HashHistory, WebHistory};
29
30mod files;
31pub use files::*;
32
33mod data_transfer;
34pub use data_transfer::*;
35
36#[cfg(all(feature = "devtools", debug_assertions))]
37mod devtools;
38
39mod hydration;
40#[allow(unused)]
41pub use hydration::*;
42
43/// Runs the app as a future that can be scheduled around the main thread.
44///
45/// Polls futures internal to the VirtualDOM, hence the async nature of this function.
46///
47/// # Example
48///
49/// ```ignore, rust
50/// let app_fut = dioxus_web::run_with_props(App, RootProps { name: String::from("foo") });
51/// wasm_bindgen_futures::spawn_local(app_fut);
52/// ```
53pub async fn run(mut virtual_dom: VirtualDom, web_config: Config) -> ! {
54    #[cfg(all(feature = "devtools", debug_assertions))]
55    let mut hotreload_rx = devtools::init(&web_config);
56
57    #[cfg(feature = "document")]
58    if let Some(history) = web_config.history.clone() {
59        virtual_dom.in_scope(ScopeId::ROOT, || dioxus_core::provide_context(history));
60    }
61
62    #[cfg(feature = "document")]
63    virtual_dom.in_runtime(document::init_document);
64
65    let runtime = virtual_dom.runtime();
66
67    // If the hydrate feature is enabled, launch the client with hydration enabled
68    let should_hydrate = web_config.hydrate || cfg!(feature = "hydrate");
69
70    let mut websys_dom = WebsysDom::new(web_config, runtime);
71
72    let mut hydration_receiver: Option<futures_channel::mpsc::UnboundedReceiver<SuspenseMessage>> =
73        None;
74
75    if should_hydrate {
76        // If we are hydrating, then the hotreload message might actually have a patch for us to apply.
77        // Let's wait for a moment to see if we get a hotreload message before we start hydrating.
78        // That way, the hydration will use the same functions that the server used to serialize the data.
79        #[cfg(all(feature = "devtools", debug_assertions))]
80        loop {
81            let mut timeout = gloo_timers::future::TimeoutFuture::new(100).fuse();
82            futures_util::select! {
83                msg = hotreload_rx.next() => {
84                    if let Some(msg) = msg {
85                        if msg.for_build_id == Some(dioxus_cli_config::build_id()) {
86                            dioxus_devtools::apply_changes(&virtual_dom, &msg);
87                        }
88                    }
89                }
90                _ = &mut timeout => {
91                    break;
92                }
93            }
94        }
95
96        #[cfg(feature = "hydrate")]
97        {
98            use dioxus_fullstack_core::HydrationContext;
99
100            websys_dom.skip_mutations = true;
101            // Get the initial hydration data from the client
102            #[wasm_bindgen::prelude::wasm_bindgen(inline_js = r#"
103                export function get_initial_hydration_data() {
104                    const decoded = atob(window.initial_dioxus_hydration_data);
105                    return Uint8Array.from(decoded, (c) => c.charCodeAt(0))
106                }
107                export function get_initial_hydration_debug_types() {
108                    return window.initial_dioxus_hydration_debug_types;
109                }
110                export function get_initial_hydration_debug_locations() {
111                    return window.initial_dioxus_hydration_debug_locations;
112                }
113            "#)]
114            extern "C" {
115                fn get_initial_hydration_data() -> js_sys::Uint8Array;
116                fn get_initial_hydration_debug_types() -> Option<Vec<String>>;
117                fn get_initial_hydration_debug_locations() -> Option<Vec<String>>;
118            }
119            let hydration_data = get_initial_hydration_data().to_vec();
120
121            // If we are running in debug mode, also get the debug types and locations
122            #[cfg(debug_assertions)]
123            let debug_types = get_initial_hydration_debug_types();
124            #[cfg(not(debug_assertions))]
125            let debug_types = None;
126            #[cfg(debug_assertions)]
127            let debug_locations = get_initial_hydration_debug_locations();
128            #[cfg(not(debug_assertions))]
129            let debug_locations = None;
130
131            let server_data =
132                HydrationContext::from_serialized(&hydration_data, debug_types, debug_locations);
133            // If the server serialized an error into the root suspense boundary, throw it into the root scope
134            if let Some(error) = server_data.error_entry().get().ok().flatten() {
135                virtual_dom.in_runtime(|| virtual_dom.runtime().throw_error(ScopeId::APP, error));
136            }
137            server_data.in_context(|| {
138                virtual_dom.in_scope(ScopeId::ROOT, || {
139                    // Provide a hydration compatible create error boundary method
140                    dioxus_core::provide_create_error_boundary(
141                        dioxus_fullstack_core::init_error_boundary,
142                    );
143                    #[cfg(feature = "document")]
144                    document::init_fullstack_document();
145                });
146                virtual_dom.rebuild(&mut websys_dom);
147            });
148            websys_dom.skip_mutations = false;
149
150            let rx = websys_dom.rehydrate(&virtual_dom).unwrap();
151            hydration_receiver = Some(rx);
152
153            #[cfg(feature = "mounted")]
154            {
155                // Flush any mounted events that were queued up while hydrating
156                websys_dom.flush_queued_mounted_events();
157            }
158        }
159        #[cfg(not(feature = "hydrate"))]
160        {
161            panic!("Hydration is not enabled. Please enable the `hydrate` feature.");
162        }
163    } else {
164        virtual_dom.rebuild(&mut websys_dom);
165
166        websys_dom.flush_edits();
167    }
168
169    loop {
170        // if virtual dom has nothing, wait for it to have something before requesting idle time
171        // if there is work then this future resolves immediately.
172        #[cfg(all(feature = "devtools", debug_assertions))]
173        let template;
174        #[allow(unused)]
175        let mut hydration_work: Option<SuspenseMessage> = None;
176
177        {
178            let work = virtual_dom.wait_for_work().fuse();
179            pin_mut!(work);
180
181            let mut hydration_receiver_iter = futures_util::stream::iter(&mut hydration_receiver)
182                .fuse()
183                .flatten();
184            let mut rx_hydration = hydration_receiver_iter.select_next_some();
185
186            #[cfg(all(feature = "devtools", debug_assertions))]
187            #[allow(unused)]
188            {
189                let mut devtools_next = hotreload_rx.select_next_some();
190                select! {
191                    _ = work => {
192                        template = None;
193                    },
194                    new_template = devtools_next => {
195                        template = Some(new_template);
196                    },
197                    hydration_data = rx_hydration => {
198                        template = None;
199                        #[cfg(feature = "hydrate")]
200                        {
201                            hydration_work = Some(hydration_data);
202                        }
203                    },
204                }
205            }
206
207            #[cfg(not(all(feature = "devtools", debug_assertions)))]
208            #[allow(unused)]
209            {
210                select! {
211                    _ = work => {},
212                    hyd = rx_hydration => {
213                        #[cfg(feature = "hydrate")]
214                        {
215                            hydration_work = Some(hyd);
216                        }
217                    }
218                }
219            }
220        }
221
222        #[cfg(all(feature = "devtools", debug_assertions))]
223        if let Some(hr_msg) = template {
224            // Replace all templates
225            dioxus_devtools::apply_changes(&virtual_dom, &hr_msg);
226
227            if !hr_msg.assets.is_empty() {
228                crate::devtools::invalidate_browser_asset_cache();
229            }
230
231            if hr_msg.for_build_id == Some(dioxus_cli_config::build_id()) {
232                devtools::show_toast(
233                    "Hot-patch success!",
234                    &format!("App successfully patched in {} ms", hr_msg.ms_elapsed),
235                    devtools::ToastLevel::Success,
236                    std::time::Duration::from_millis(2000),
237                    false,
238                );
239            }
240        }
241
242        #[cfg(feature = "hydrate")]
243        if let Some(hydration_data) = hydration_work {
244            websys_dom.rehydrate_streaming(hydration_data, &mut virtual_dom);
245        }
246
247        // Todo: This is currently disabled because it has a negative impact on response times for events but it could be re-enabled for tasks
248        // Jank free rendering
249        //
250        // 1. wait for the browser to give us "idle" time
251        // 2. During idle time, diff the dom
252        // 3. Stop diffing if the deadline is exceeded
253        // 4. Wait for the animation frame to patch the dom
254
255        // wait for the mainthread to schedule us in
256        // let deadline = work_loop.wait_for_idle_time().await;
257
258        // run the virtualdom work phase until the frame deadline is reached
259        virtual_dom.render_immediate(&mut websys_dom);
260
261        // wait for the animation frame to fire so we can apply our changes
262        // work_loop.wait_for_raf().await;
263
264        websys_dom.flush_edits();
265    }
266}