Skip to main content

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::{FutureExt, StreamExt, pin_mut, select};
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                        && msg.for_build_id == Some(dioxus_cli_config::build_id()) {
86                            dioxus_devtools::apply_changes(&virtual_dom, &msg);
87                        }
88                }
89                _ = &mut timeout => {
90                    break;
91                }
92            }
93        }
94
95        #[cfg(feature = "hydrate")]
96        {
97            use dioxus_fullstack_core::HydrationContext;
98
99            websys_dom.skip_mutations = true;
100            // Get the initial hydration data from the client
101            #[wasm_bindgen::prelude::wasm_bindgen(inline_js = r#"
102                export function get_initial_hydration_data() {
103                    const decoded = atob(window.initial_dioxus_hydration_data);
104                    return Uint8Array.from(decoded, (c) => c.charCodeAt(0))
105                }
106                export function get_initial_hydration_debug_types() {
107                    return window.initial_dioxus_hydration_debug_types;
108                }
109                export function get_initial_hydration_debug_locations() {
110                    return window.initial_dioxus_hydration_debug_locations;
111                }
112            "#)]
113            extern "C" {
114                fn get_initial_hydration_data() -> js_sys::Uint8Array;
115                fn get_initial_hydration_debug_types() -> Option<Vec<String>>;
116                fn get_initial_hydration_debug_locations() -> Option<Vec<String>>;
117            }
118            let hydration_data = get_initial_hydration_data().to_vec();
119
120            // If we are running in debug mode, also get the debug types and locations
121            #[cfg(debug_assertions)]
122            let debug_types = get_initial_hydration_debug_types();
123            #[cfg(not(debug_assertions))]
124            let debug_types = None;
125            #[cfg(debug_assertions)]
126            let debug_locations = get_initial_hydration_debug_locations();
127            #[cfg(not(debug_assertions))]
128            let debug_locations = None;
129
130            let server_data =
131                HydrationContext::from_serialized(&hydration_data, debug_types, debug_locations);
132            // If the server serialized an error into the root suspense boundary, throw it into the root scope
133            if let Some(error) = server_data.error_entry().get().ok().flatten() {
134                virtual_dom.in_runtime(|| virtual_dom.runtime().throw_error(ScopeId::APP, error));
135            }
136            server_data.in_context(|| {
137                virtual_dom.in_scope(ScopeId::ROOT, || {
138                    // Provide a hydration compatible create error boundary method
139                    dioxus_core::provide_create_error_boundary(
140                        dioxus_fullstack_core::init_error_boundary,
141                    );
142                    #[cfg(feature = "document")]
143                    document::init_fullstack_document();
144                });
145                virtual_dom.rebuild(&mut websys_dom);
146            });
147            websys_dom.skip_mutations = false;
148
149            let rx = websys_dom.rehydrate(&virtual_dom).unwrap();
150            hydration_receiver = Some(rx);
151
152            #[cfg(feature = "mounted")]
153            {
154                // Flush any mounted events that were queued up while hydrating
155                websys_dom.flush_queued_mounted_events();
156            }
157        }
158        #[cfg(not(feature = "hydrate"))]
159        {
160            panic!("Hydration is not enabled. Please enable the `hydrate` feature.");
161        }
162    } else {
163        virtual_dom.rebuild(&mut websys_dom);
164
165        websys_dom.flush_edits();
166    }
167
168    loop {
169        // if virtual dom has nothing, wait for it to have something before requesting idle time
170        // if there is work then this future resolves immediately.
171        #[cfg(all(feature = "devtools", debug_assertions))]
172        let template;
173        #[allow(unused)]
174        let mut hydration_work: Option<SuspenseMessage> = None;
175
176        {
177            let work = virtual_dom.wait_for_work().fuse();
178            pin_mut!(work);
179
180            let mut hydration_receiver_iter = futures_util::stream::iter(&mut hydration_receiver)
181                .fuse()
182                .flatten();
183            let mut rx_hydration = hydration_receiver_iter.select_next_some();
184
185            #[cfg(all(feature = "devtools", debug_assertions))]
186            #[allow(unused)]
187            {
188                let mut devtools_next = hotreload_rx.select_next_some();
189                select! {
190                    _ = work => {
191                        template = None;
192                    },
193                    new_template = devtools_next => {
194                        template = Some(new_template);
195                    },
196                    hydration_data = rx_hydration => {
197                        template = None;
198                        #[cfg(feature = "hydrate")]
199                        {
200                            hydration_work = Some(hydration_data);
201                        }
202                    },
203                }
204            }
205
206            #[cfg(not(all(feature = "devtools", debug_assertions)))]
207            #[allow(unused)]
208            {
209                select! {
210                    _ = work => {},
211                    hyd = rx_hydration => {
212                        #[cfg(feature = "hydrate")]
213                        {
214                            hydration_work = Some(hyd);
215                        }
216                    }
217                }
218            }
219        }
220
221        #[cfg(all(feature = "devtools", debug_assertions))]
222        if let Some(hr_msg) = template {
223            // Replace all templates
224            dioxus_devtools::apply_changes(&virtual_dom, &hr_msg);
225
226            if !hr_msg.assets.is_empty() {
227                crate::devtools::invalidate_browser_asset_cache();
228            }
229
230            if hr_msg.for_build_id == Some(dioxus_cli_config::build_id()) {
231                devtools::show_toast(
232                    "Hot-patch success!",
233                    &format!("App successfully patched in {} ms", hr_msg.ms_elapsed),
234                    devtools::ToastLevel::Success,
235                    std::time::Duration::from_millis(2000),
236                    false,
237                );
238            }
239        }
240
241        #[cfg(feature = "hydrate")]
242        if let Some(hydration_data) = hydration_work {
243            websys_dom.rehydrate_streaming(hydration_data, &mut virtual_dom);
244        }
245
246        // Todo: This is currently disabled because it has a negative impact on response times for events but it could be re-enabled for tasks
247        // Jank free rendering
248        //
249        // 1. wait for the browser to give us "idle" time
250        // 2. During idle time, diff the dom
251        // 3. Stop diffing if the deadline is exceeded
252        // 4. Wait for the animation frame to patch the dom
253
254        // wait for the mainthread to schedule us in
255        // let deadline = work_loop.wait_for_idle_time().await;
256
257        // run the virtualdom work phase until the frame deadline is reached
258        virtual_dom.render_immediate(&mut websys_dom);
259
260        // wait for the animation frame to fire so we can apply our changes
261        // work_loop.wait_for_raf().await;
262
263        websys_dom.flush_edits();
264    }
265}