From de72d85391a44ecb0452ab21931f924b56fef124 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 14 Jul 2023 17:23:12 -0700 Subject: [PATCH 1/9] create server_cached function --- .../examples/axum-hello-world/src/main.rs | 24 +++++++++- .../deserialize.rs} | 27 ++++++++++- .../src/{props_html => html_storage}/mod.rs | 48 +++++++++++++++++-- .../serialize.rs} | 22 +++++++-- packages/fullstack/src/lib.rs | 8 +++- packages/fullstack/src/render.rs | 30 +++++++++++- packages/fullstack/src/server_context.rs | 19 ++++++++ packages/fullstack/src/use_server/mod.rs | 18 +++++++ 8 files changed, 182 insertions(+), 14 deletions(-) rename packages/fullstack/src/{props_html/deserialize_props.rs => html_storage/deserialize.rs} (55%) rename packages/fullstack/src/{props_html => html_storage}/mod.rs (50%) rename packages/fullstack/src/{props_html/serialize_props.rs => html_storage/serialize.rs} (50%) create mode 100644 packages/fullstack/src/use_server/mod.rs diff --git a/packages/fullstack/examples/axum-hello-world/src/main.rs b/packages/fullstack/examples/axum-hello-world/src/main.rs index 739a24b1c..9d8d232dc 100644 --- a/packages/fullstack/examples/axum-hello-world/src/main.rs +++ b/packages/fullstack/examples/axum-hello-world/src/main.rs @@ -16,10 +16,32 @@ struct AppProps { } fn app(cx: Scope) -> Element { + let state1 = server_cached(|| { + #[cfg(not(feature = "ssr"))] + panic!(); + 12345 + }); + assert_eq!(state1, 12345); + let state2 = server_cached(|| { + #[cfg(not(feature = "ssr"))] + panic!(); + 123456 + }); + assert_eq!(state2, 123456); + let state3 = server_cached(|| { + #[cfg(not(feature = "ssr"))] + panic!(); + 1234567 + }); + assert_eq!(state3, 1234567); + let mut count = use_state(cx, || cx.props.count); let text = use_state(cx, || "...".to_string()); cx.render(rsx! { + div { + "Server state: {state1}, {state2}, {state3}" + } h1 { "High-Five counter: {count}" } button { onclick: move |_| count += 1, "Up high!" } button { onclick: move |_| count -= 1, "Down low!" } @@ -42,7 +64,7 @@ fn app(cx: Scope) -> Element { #[server(PostServerData)] async fn post_server_data(data: String) -> Result<(), ServerFnError> { - let axum::extract::Host(host): axum::extract::Host = extract()?; + let axum::extract::Host(host): axum::extract::Host = extract().await?; println!("Server received: {}", data); println!("{:?}", host); diff --git a/packages/fullstack/src/props_html/deserialize_props.rs b/packages/fullstack/src/html_storage/deserialize.rs similarity index 55% rename from packages/fullstack/src/props_html/deserialize_props.rs rename to packages/fullstack/src/html_storage/deserialize.rs index f5c805865..3a0fc51df 100644 --- a/packages/fullstack/src/props_html/deserialize_props.rs +++ b/packages/fullstack/src/html_storage/deserialize.rs @@ -3,6 +3,8 @@ use serde::de::DeserializeOwned; use base64::engine::general_purpose::STANDARD; use base64::Engine; +use super::HTMLDataCursor; + #[allow(unused)] pub(crate) fn serde_from_bytes(string: &[u8]) -> Option { let decompressed = STANDARD.decode(string).ok()?; @@ -10,6 +12,29 @@ pub(crate) fn serde_from_bytes(string: &[u8]) -> Option postcard::from_bytes(&decompressed).ok() } +static SERVER_DATA: once_cell::sync::Lazy> = + once_cell::sync::Lazy::new(|| { + #[cfg(target_arch = "wasm32")] + { + let attribute = web_sys::window()? + .document()? + .get_element_by_id("dioxus-storage-data")? + .get_attribute("data-serialized")?; + + let data: super::HTMLData = serde_from_bytes(attribute.as_bytes())?; + + Some(data.cursor()) + } + #[cfg(not(target_arch = "wasm32"))] + { + None + } + }); + +pub(crate) fn take_server_data() -> Option { + SERVER_DATA.as_ref()?.take() +} + #[cfg(not(feature = "ssr"))] /// Get the props from the document. This is only available in the browser. /// @@ -23,7 +48,7 @@ pub fn get_root_props_from_document() -> Option { { let attribute = web_sys::window()? .document()? - .get_element_by_id("dioxus-storage")? + .get_element_by_id("dioxus-storage-props")? .get_attribute("data-serialized")?; serde_from_bytes(attribute.as_bytes()) diff --git a/packages/fullstack/src/props_html/mod.rs b/packages/fullstack/src/html_storage/mod.rs similarity index 50% rename from packages/fullstack/src/props_html/mod.rs rename to packages/fullstack/src/html_storage/mod.rs index 143c40231..dd40ce587 100644 --- a/packages/fullstack/src/props_html/mod.rs +++ b/packages/fullstack/src/html_storage/mod.rs @@ -1,6 +1,46 @@ -pub(crate) mod deserialize_props; +use std::sync::atomic::AtomicUsize; -pub(crate) mod serialize_props; +use serde::{de::DeserializeOwned, Serialize}; + +pub(crate) mod deserialize; + +pub(crate) mod serialize; + +#[derive(serde::Serialize, serde::Deserialize, Default)] +pub(crate) struct HTMLData { + pub data: Vec>, +} + +impl HTMLData { + pub(crate) fn push(&mut self, value: &T) { + let serialized = postcard::to_allocvec(value).unwrap(); + self.data.push(serialized); + } + + pub(crate) fn cursor(self) -> HTMLDataCursor { + HTMLDataCursor { + data: self.data, + index: AtomicUsize::new(0), + } + } +} + +pub(crate) struct HTMLDataCursor { + data: Vec>, + index: AtomicUsize, +} + +impl HTMLDataCursor { + pub fn take(&self) -> Option { + let current = self.index.load(std::sync::atomic::Ordering::SeqCst); + if current >= self.data.len() { + return None; + } + let mut cursor = &self.data[current]; + self.index.fetch_add(1, std::sync::atomic::Ordering::SeqCst); + Some(postcard::from_bytes(&mut cursor).unwrap()) + } +} #[test] fn serialized_and_deserializes() { @@ -37,7 +77,7 @@ fn serialized_and_deserializes() { }; y ]; - serialize_props::serde_to_writable(&data, &mut as_string).unwrap(); + serialize::serde_to_writable(&data, &mut as_string).unwrap(); println!("{:?}", as_string); println!( @@ -47,7 +87,7 @@ fn serialized_and_deserializes() { println!("serialized size: {}", to_allocvec(&data).unwrap().len()); println!("compressed size: {}", as_string.len()); - let decoded: Vec = deserialize_props::serde_from_bytes(&as_string).unwrap(); + let decoded: Vec = deserialize::serde_from_bytes(&as_string).unwrap(); assert_eq!(data, decoded); } } diff --git a/packages/fullstack/src/props_html/serialize_props.rs b/packages/fullstack/src/html_storage/serialize.rs similarity index 50% rename from packages/fullstack/src/props_html/serialize_props.rs rename to packages/fullstack/src/html_storage/serialize.rs index 0c6ae3c49..7f0a7efc7 100644 --- a/packages/fullstack/src/props_html/serialize_props.rs +++ b/packages/fullstack/src/html_storage/serialize.rs @@ -15,12 +15,26 @@ pub(crate) fn serde_to_writable( #[cfg(feature = "ssr")] /// Encode data into a element. This is inteded to be used in the server to send data to the client. -pub(crate) fn encode_in_element( - data: T, +pub(crate) fn encode_props_in_element( + data: &T, write_to: &mut impl std::io::Write, ) -> std::io::Result<()> { - write_to - .write_all(r#""#.as_bytes()) +} + +#[cfg(feature = "ssr")] +/// Encode data into a element. This is inteded to be used in the server to send data to the client. +pub(crate) fn encode_in_element( + data: &super::HTMLData, + write_to: &mut impl std::io::Write, +) -> std::io::Result<()> { + write_to.write_all( + r#""#.as_bytes()) } diff --git a/packages/fullstack/src/lib.rs b/packages/fullstack/src/lib.rs index 4728c1846..b9318d0d6 100644 --- a/packages/fullstack/src/lib.rs +++ b/packages/fullstack/src/lib.rs @@ -5,7 +5,7 @@ pub use once_cell; -mod props_html; +mod html_storage; #[cfg(feature = "router")] pub mod router; @@ -26,6 +26,7 @@ mod serve_config; #[cfg(feature = "ssr")] mod server_context; mod server_fn; +mod use_server; /// A prelude of commonly used items in dioxus-fullstack. pub mod prelude { @@ -36,7 +37,7 @@ pub mod prelude { #[cfg(feature = "warp")] pub use crate::adapters::warp_adapter::*; #[cfg(not(feature = "ssr"))] - pub use crate::props_html::deserialize_props::get_root_props_from_document; + pub use crate::html_storage::deserialize::get_root_props_from_document; #[cfg(all(feature = "ssr", feature = "router"))] pub use crate::render::pre_cache_static_routes_with_props; #[cfg(feature = "ssr")] @@ -52,9 +53,12 @@ pub mod prelude { pub use crate::server_fn::DioxusServerFn; #[cfg(feature = "ssr")] pub use crate::server_fn::{ServerFnMiddleware, ServerFnTraitObj, ServerFunction}; + use crate::use_server; pub use crate::{launch, launch_router}; pub use dioxus_server_macro::*; #[cfg(feature = "ssr")] pub use dioxus_ssr::incremental::IncrementalRendererConfig; pub use server_fn::{self, ServerFn as _, ServerFnError}; + + pub use use_server::server_cached; } diff --git a/packages/fullstack/src/render.rs b/packages/fullstack/src/render.rs index fbd4bd7f3..9660617d6 100644 --- a/packages/fullstack/src/render.rs +++ b/packages/fullstack/src/render.rs @@ -27,7 +27,10 @@ impl SsrRendererPool { to: &mut WriteBuffer, server_context: &DioxusServerContext, ) -> Result { - let wrapper = FullstackRenderer { cfg }; + let wrapper = FullstackRenderer { + cfg, + server_context: server_context.clone(), + }; match self { Self::Renderer(pool) => { let server_context = Box::new(server_context.clone()); @@ -126,6 +129,7 @@ impl SSRState { struct FullstackRenderer<'a, P: Clone + Send + Sync + 'static> { cfg: &'a ServeConfig

, + server_context: DioxusServerContext, } impl<'a, P: Clone + Serialize + Send + Sync + 'static> dioxus_ssr::incremental::WrapBody @@ -147,7 +151,29 @@ impl<'a, P: Clone + Serialize + Send + Sync + 'static> dioxus_ssr::incremental:: to: &mut R, ) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> { // serialize the props - crate::props_html::serialize_props::encode_in_element(&self.cfg.props, to)?; + crate::html_storage::serialize::encode_props_in_element(&self.cfg.props, to)?; + // serialize the server state + crate::html_storage::serialize::encode_in_element( + &*self.server_context.html_data().map_err(|err| { + dioxus_ssr::incremental::IncrementalRendererError::Other(Box::new({ + #[derive(Debug)] + struct HTMLDataReadError; + + impl std::fmt::Display for HTMLDataReadError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str( + "Failed to read the server data to serialize it into the HTML", + ) + } + } + + impl std::error::Error for HTMLDataReadError {} + + HTMLDataReadError + })) + })?, + to, + )?; #[cfg(all(debug_assertions, feature = "hot-reload"))] { diff --git a/packages/fullstack/src/server_context.rs b/packages/fullstack/src/server_context.rs index fcd0ef66b..bf84bd524 100644 --- a/packages/fullstack/src/server_context.rs +++ b/packages/fullstack/src/server_context.rs @@ -1,3 +1,4 @@ +use crate::html_storage::HTMLData; pub use server_fn_impl::*; use std::sync::Arc; use std::sync::RwLock; @@ -13,6 +14,7 @@ pub struct DioxusServerContext { >, response_parts: std::sync::Arc>, pub(crate) parts: Arc>, + html_data: Arc>, } #[allow(clippy::derivable_impls)] @@ -24,6 +26,7 @@ impl Default for DioxusServerContext { http::response::Response::new(()).into_parts().0, )), parts: std::sync::Arc::new(RwLock::new(http::request::Request::new(()).into_parts().0)), + html_data: Arc::new(RwLock::new(HTMLData::default())), } } } @@ -45,6 +48,7 @@ mod server_fn_impl { response_parts: std::sync::Arc::new(RwLock::new( http::response::Response::new(()).into_parts().0, )), + html_data: Arc::new(RwLock::new(HTMLData::default())), } } @@ -100,6 +104,21 @@ mod server_fn_impl { ) -> Result { T::from_request(self).await } + + /// Insert some data into the html data store + pub(crate) async fn push_html_data( + &self, + value: &T, + ) -> Result<(), PoisonError>> { + self.html_data.write().map(|mut map| { + map.push(value); + }) + } + + /// Get the html data store + pub(crate) fn html_data(&self) -> LockResult> { + self.html_data.read() + } } } diff --git a/packages/fullstack/src/use_server/mod.rs b/packages/fullstack/src/use_server/mod.rs new file mode 100644 index 000000000..dc47b5bca --- /dev/null +++ b/packages/fullstack/src/use_server/mod.rs @@ -0,0 +1,18 @@ +use serde::{de::DeserializeOwned, Serialize}; + +/// TODO: Document this +pub fn server_cached(server_fn: impl Fn() -> O) -> O { + #[cfg(feature = "ssr")] + { + let data = + crate::html_storage::deserialize::take_server_data().unwrap_or_else(|| server_fn()); + let sc = crate::prelude::server_context(); + sc.push_html_data(&data); + data + } + #[cfg(not(feature = "ssr"))] + { + let data = server_fn(); + data + } +} From 040b9d15a8e0e35b259e0fb0b520080d321052f5 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 14 Jul 2023 17:39:46 -0700 Subject: [PATCH 2/9] document from_server --- packages/fullstack/src/lib.rs | 2 +- packages/fullstack/src/use_server/mod.rs | 20 ++++++++++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/packages/fullstack/src/lib.rs b/packages/fullstack/src/lib.rs index b9318d0d6..72f4340c9 100644 --- a/packages/fullstack/src/lib.rs +++ b/packages/fullstack/src/lib.rs @@ -60,5 +60,5 @@ pub mod prelude { pub use dioxus_ssr::incremental::IncrementalRendererConfig; pub use server_fn::{self, ServerFn as _, ServerFnError}; - pub use use_server::server_cached; + pub use use_server::from_server; } diff --git a/packages/fullstack/src/use_server/mod.rs b/packages/fullstack/src/use_server/mod.rs index dc47b5bca..f0f76d16f 100644 --- a/packages/fullstack/src/use_server/mod.rs +++ b/packages/fullstack/src/use_server/mod.rs @@ -1,7 +1,23 @@ use serde::{de::DeserializeOwned, Serialize}; -/// TODO: Document this -pub fn server_cached(server_fn: impl Fn() -> O) -> O { +/// This allows you to send data from the server to the client. The data is serialized into the HTML on the server and hydrated on the client. +/// +/// When you run this function on the client, you need to be careful to insure the order you run it initially is the same order you run it on the server. +/// +/// If Dioxus fullstack cannot find the data on the client, it will run the closure again to get the data. +/// +/// # Example +/// ```rust +/// use dioxus::prelude::*; +/// use dioxus_fullstack::prelude::*; +/// +/// fn app(cx: Scope) -> Element { +/// let state1 = use_state(cx, || from_server(|| { +/// 1234 +/// })); +/// } +/// ``` +pub fn from_server(server_fn: impl Fn() -> O) -> O { #[cfg(feature = "ssr")] { let data = From b05fb16155a93baf9f00afe5c531c8453942d4b6 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 17 Jul 2023 09:48:35 -0700 Subject: [PATCH 3/9] fix server_cached function --- packages/fullstack/src/html_storage/mod.rs | 15 ++++++++++++++- packages/fullstack/src/render.rs | 2 +- packages/fullstack/src/server_context.rs | 2 +- packages/fullstack/src/use_server/mod.rs | 10 +++++----- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/packages/fullstack/src/html_storage/mod.rs b/packages/fullstack/src/html_storage/mod.rs index dd40ce587..f0fbca127 100644 --- a/packages/fullstack/src/html_storage/mod.rs +++ b/packages/fullstack/src/html_storage/mod.rs @@ -1,3 +1,5 @@ +#![allow(unused)] + use std::sync::atomic::AtomicUsize; use serde::{de::DeserializeOwned, Serialize}; @@ -34,11 +36,22 @@ impl HTMLDataCursor { pub fn take(&self) -> Option { let current = self.index.load(std::sync::atomic::Ordering::SeqCst); if current >= self.data.len() { + log::error!( + "Tried to take more data than was available, len: {}, index: {}", + self.data.len(), + current + ); return None; } let mut cursor = &self.data[current]; self.index.fetch_add(1, std::sync::atomic::Ordering::SeqCst); - Some(postcard::from_bytes(&mut cursor).unwrap()) + match postcard::from_bytes(&mut cursor) { + Ok(x) => Some(x), + Err(e) => { + log::error!("Error deserializing data: {:?}", e); + None + } + } } } diff --git a/packages/fullstack/src/render.rs b/packages/fullstack/src/render.rs index 9660617d6..c71d0240c 100644 --- a/packages/fullstack/src/render.rs +++ b/packages/fullstack/src/render.rs @@ -154,7 +154,7 @@ impl<'a, P: Clone + Serialize + Send + Sync + 'static> dioxus_ssr::incremental:: crate::html_storage::serialize::encode_props_in_element(&self.cfg.props, to)?; // serialize the server state crate::html_storage::serialize::encode_in_element( - &*self.server_context.html_data().map_err(|err| { + &*self.server_context.html_data().map_err(|_| { dioxus_ssr::incremental::IncrementalRendererError::Other(Box::new({ #[derive(Debug)] struct HTMLDataReadError; diff --git a/packages/fullstack/src/server_context.rs b/packages/fullstack/src/server_context.rs index bf84bd524..9315434b8 100644 --- a/packages/fullstack/src/server_context.rs +++ b/packages/fullstack/src/server_context.rs @@ -106,7 +106,7 @@ mod server_fn_impl { } /// Insert some data into the html data store - pub(crate) async fn push_html_data( + pub(crate) fn push_html_data( &self, value: &T, ) -> Result<(), PoisonError>> { diff --git a/packages/fullstack/src/use_server/mod.rs b/packages/fullstack/src/use_server/mod.rs index f0f76d16f..0cf15927d 100644 --- a/packages/fullstack/src/use_server/mod.rs +++ b/packages/fullstack/src/use_server/mod.rs @@ -20,15 +20,15 @@ use serde::{de::DeserializeOwned, Serialize}; pub fn from_server(server_fn: impl Fn() -> O) -> O { #[cfg(feature = "ssr")] { - let data = - crate::html_storage::deserialize::take_server_data().unwrap_or_else(|| server_fn()); + let data = server_fn(); let sc = crate::prelude::server_context(); - sc.push_html_data(&data); + if let Err(err) = sc.push_html_data(&data) { + log::error!("Failed to push HTML data: {}", err); + } data } #[cfg(not(feature = "ssr"))] { - let data = server_fn(); - data + crate::html_storage::deserialize::take_server_data().unwrap_or_else(|| server_fn()) } } From dd5d974aeb5e3b7e8de29f0614461eb764b32e0e Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 17 Jul 2023 16:48:54 -0700 Subject: [PATCH 4/9] fix suspense --- packages/core/src/scope_arena.rs | 8 +- .../examples/axum-hello-world/Cargo.toml | 3 + .../examples/axum-hello-world/src/main.rs | 44 ++-- packages/fullstack/src/hooks/mod.rs | 2 + .../mod.rs => hooks/server_cached.rs} | 2 +- packages/fullstack/src/hooks/server_future.rs | 146 +++++++++++++ packages/fullstack/src/lib.rs | 6 +- packages/fullstack/src/render.rs | 195 +++++++++++++----- packages/fullstack/src/server_context.rs | 2 +- packages/router/src/incremental.rs | 13 +- packages/ssr/src/fs_cache.rs | 7 +- packages/ssr/src/incremental.rs | 33 ++- 12 files changed, 351 insertions(+), 110 deletions(-) create mode 100644 packages/fullstack/src/hooks/mod.rs rename packages/fullstack/src/{use_server/mod.rs => hooks/server_cached.rs} (91%) create mode 100644 packages/fullstack/src/hooks/server_future.rs diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index d058e34de..f1635dd61 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -83,12 +83,12 @@ impl VirtualDom { id: scope.id, }); - if matches!(allocated, RenderReturn::Aborted(_)) { - if scope.suspended.get() { + if scope.suspended.get() { + if matches!(allocated, RenderReturn::Aborted(_)) { self.suspended_scopes.insert(scope.id); - } else if !self.suspended_scopes.is_empty() { - _ = self.suspended_scopes.remove(&scope.id); } + } else { + _ = self.suspended_scopes.remove(&scope.id); } // rebind the lifetime now that its stored internally diff --git a/packages/fullstack/examples/axum-hello-world/Cargo.toml b/packages/fullstack/examples/axum-hello-world/Cargo.toml index 9761864bb..3746e0b07 100644 --- a/packages/fullstack/examples/axum-hello-world/Cargo.toml +++ b/packages/fullstack/examples/axum-hello-world/Cargo.toml @@ -16,6 +16,9 @@ serde = "1.0.159" execute = "0.2.12" tower-http = { version = "0.4.1", features = ["auth"] } simple_logger = "4.2.0" +wasm-logger = "0.2.0" +log.workspace = true +cargo-cache = "0.8.3" [features] default = [] diff --git a/packages/fullstack/examples/axum-hello-world/src/main.rs b/packages/fullstack/examples/axum-hello-world/src/main.rs index 9d8d232dc..09bdcf3ce 100644 --- a/packages/fullstack/examples/axum-hello-world/src/main.rs +++ b/packages/fullstack/examples/axum-hello-world/src/main.rs @@ -16,31 +16,30 @@ struct AppProps { } fn app(cx: Scope) -> Element { - let state1 = server_cached(|| { - #[cfg(not(feature = "ssr"))] - panic!(); - 12345 - }); - assert_eq!(state1, 12345); - let state2 = server_cached(|| { - #[cfg(not(feature = "ssr"))] - panic!(); - 123456 - }); - assert_eq!(state2, 123456); - let state3 = server_cached(|| { - #[cfg(not(feature = "ssr"))] - panic!(); - 1234567 - }); - assert_eq!(state3, 1234567); + render! { + Child {} + } +} - let mut count = use_state(cx, || cx.props.count); +fn Child(cx: Scope) -> Element { + let state = use_server_future(cx, (), |()| async move { + #[cfg(not(feature = "ssr"))] + panic!(); + #[cfg(feature = "ssr")] + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + return 1; + })?; + + log::info!("running child"); + let state = state.value(); + log::info!("child state: {:?}", state); + + let mut count = use_state(cx, || 0); let text = use_state(cx, || "...".to_string()); cx.render(rsx! { div { - "Server state: {state1}, {state2}, {state3}" + "Server state: {state}" } h1 { "High-Five counter: {count}" } button { onclick: move |_| count += 1, "Up high!" } @@ -77,6 +76,11 @@ async fn get_server_data() -> Result { } fn main() { + #[cfg(feature = "web")] + wasm_logger::init(wasm_logger::Config::default()); + #[cfg(feature = "ssr")] + simple_logger::SimpleLogger::new().init().unwrap(); + launch!(@([127, 0, 0, 1], 8080), app, { serve_cfg: ServeConfigBuilder::new(app, AppProps { count: 0 }), }); diff --git a/packages/fullstack/src/hooks/mod.rs b/packages/fullstack/src/hooks/mod.rs new file mode 100644 index 000000000..f42ce3f78 --- /dev/null +++ b/packages/fullstack/src/hooks/mod.rs @@ -0,0 +1,2 @@ +pub mod server_cached; +pub mod server_future; diff --git a/packages/fullstack/src/use_server/mod.rs b/packages/fullstack/src/hooks/server_cached.rs similarity index 91% rename from packages/fullstack/src/use_server/mod.rs rename to packages/fullstack/src/hooks/server_cached.rs index 0cf15927d..e28783b77 100644 --- a/packages/fullstack/src/use_server/mod.rs +++ b/packages/fullstack/src/hooks/server_cached.rs @@ -17,7 +17,7 @@ use serde::{de::DeserializeOwned, Serialize}; /// })); /// } /// ``` -pub fn from_server(server_fn: impl Fn() -> O) -> O { +pub fn server_cached(server_fn: impl Fn() -> O) -> O { #[cfg(feature = "ssr")] { let data = server_fn(); diff --git a/packages/fullstack/src/hooks/server_future.rs b/packages/fullstack/src/hooks/server_future.rs new file mode 100644 index 000000000..2e3789ab7 --- /dev/null +++ b/packages/fullstack/src/hooks/server_future.rs @@ -0,0 +1,146 @@ +use dioxus::prelude::*; +use serde::{de::DeserializeOwned, Serialize}; +use std::any::Any; +use std::cell::Cell; +use std::cell::Ref; +use std::cell::RefCell; +use std::future::Future; +use std::rc::Rc; +use std::sync::Arc; + +/// A future that resolves to a value. +/// +/// This runs the future only once - though the future may be regenerated +/// through the [`UseServerFuture::restart`] method. +/// +/// This is commonly used for components that cannot be rendered until some +/// asynchronous operation has completed. +/// +/// Whenever the hooks dependencies change, the future will be re-evaluated. +/// If a future is pending when the dependencies change, the previous future +/// will be allowed to continue +/// +/// - dependencies: a tuple of references to values that are PartialEq + Clone +pub fn use_server_future( + cx: &ScopeState, + dependencies: D, + future: impl FnOnce(D::Out) -> F, +) -> Option<&UseServerFuture> +where + T: 'static + Serialize + DeserializeOwned, + F: Future + 'static, + D: UseFutureDep, +{ + let state = cx.use_hook(move || UseServerFuture { + update: cx.schedule_update(), + needs_regen: Cell::new(true), + value: Default::default(), + task: Cell::new(None), + dependencies: Vec::new(), + }); + + let first_run = { state.value.borrow().as_ref().is_none() }; + + if dependencies.clone().apply(&mut state.dependencies) || state.needs_regen.get() { + // We don't need regen anymore + state.needs_regen.set(false); + + // Create the new future + let fut = future(dependencies.out()); + + // Clone in our cells + let value = state.value.clone(); + let schedule_update = state.update.clone(); + + // Cancel the current future + if let Some(current) = state.task.take() { + cx.remove_future(current); + } + + state.task.set(Some(cx.push_future(async move { + let data; + #[cfg(feature = "ssr")] + { + data = fut.await; + if first_run { + if let Err(err) = crate::prelude::server_context().push_html_data(&data) { + log::error!("Failed to push HTML data: {}", err); + }; + } + } + #[cfg(not(feature = "ssr"))] + { + data = match crate::html_storage::deserialize::take_server_data() { + Some(data) => data, + None => fut.await, + }; + } + *value.borrow_mut() = Some(Box::new(data)); + + schedule_update(); + }))); + } + + if first_run { + log::trace!("Suspending first run of use_server_future"); + cx.suspend(); + None + } else { + Some(state) + } +} + +pub enum FutureState<'a, T> { + Pending, + Complete(&'a T), + Regenerating(&'a T), // the old value +} + +pub struct UseServerFuture { + update: Arc, + needs_regen: Cell, + task: Cell>, + dependencies: Vec>, + value: Rc>>>, +} + +pub enum UseFutureState<'a, T> { + Pending, + Complete(&'a T), + Reloading(&'a T), +} + +impl UseServerFuture { + /// Restart the future with new dependencies. + /// + /// Will not cancel the previous future, but will ignore any values that it + /// generates. + pub fn restart(&self) { + self.needs_regen.set(true); + (self.update)(); + } + + /// Forcefully cancel a future + pub fn cancel(&self, cx: &ScopeState) { + if let Some(task) = self.task.take() { + cx.remove_future(task); + } + } + + /// Return any value, even old values if the future has not yet resolved. + /// + /// If the future has never completed, the returned value will be `None`. + pub fn value(&self) -> Ref<'_, T> { + Ref::map(self.value.borrow(), |v| v.as_deref().unwrap()) + } + + /// Get the ID of the future in Dioxus' internal scheduler + pub fn task(&self) -> Option { + self.task.get() + } + + /// Get the current state of the future. + pub fn reloading(&self) -> bool { + self.task.get().is_some() + } +} diff --git a/packages/fullstack/src/lib.rs b/packages/fullstack/src/lib.rs index 72f4340c9..04b65ba4c 100644 --- a/packages/fullstack/src/lib.rs +++ b/packages/fullstack/src/lib.rs @@ -14,6 +14,7 @@ pub mod router; mod adapters; #[cfg(feature = "ssr")] pub use adapters::*; +mod hooks; #[cfg(all(debug_assertions, feature = "hot-reload", feature = "ssr"))] mod hot_reload; pub mod launch; @@ -26,7 +27,6 @@ mod serve_config; #[cfg(feature = "ssr")] mod server_context; mod server_fn; -mod use_server; /// A prelude of commonly used items in dioxus-fullstack. pub mod prelude { @@ -36,6 +36,7 @@ pub mod prelude { pub use crate::adapters::salvo_adapter::*; #[cfg(feature = "warp")] pub use crate::adapters::warp_adapter::*; + use crate::hooks; #[cfg(not(feature = "ssr"))] pub use crate::html_storage::deserialize::get_root_props_from_document; #[cfg(all(feature = "ssr", feature = "router"))] @@ -53,12 +54,11 @@ pub mod prelude { pub use crate::server_fn::DioxusServerFn; #[cfg(feature = "ssr")] pub use crate::server_fn::{ServerFnMiddleware, ServerFnTraitObj, ServerFunction}; - use crate::use_server; pub use crate::{launch, launch_router}; pub use dioxus_server_macro::*; #[cfg(feature = "ssr")] pub use dioxus_ssr::incremental::IncrementalRendererConfig; pub use server_fn::{self, ServerFn as _, ServerFnError}; - pub use use_server::from_server; + pub use hooks::{server_cached::server_cached, server_future::use_server_future}; } diff --git a/packages/fullstack/src/render.rs b/packages/fullstack/src/render.rs index c71d0240c..c6a13c347 100644 --- a/packages/fullstack/src/render.rs +++ b/packages/fullstack/src/render.rs @@ -2,19 +2,22 @@ use std::sync::Arc; +use crate::server_context::SERVER_CONTEXT; use dioxus::prelude::VirtualDom; use dioxus_ssr::{ incremental::{IncrementalRendererConfig, RenderFreshness, WrapBody}, Renderer, }; use serde::Serialize; +use std::sync::RwLock; +use tokio::task::spawn_blocking; use crate::{prelude::*, server_context::with_server_context}; use dioxus::prelude::*; enum SsrRendererPool { - Renderer(object_pool::Pool), - Incremental(object_pool::Pool), + Renderer(RwLock>), + Incremental(RwLock>), } impl SsrRendererPool { @@ -24,49 +27,130 @@ impl SsrRendererPool { route: String, component: Component

, props: P, - to: &mut WriteBuffer, server_context: &DioxusServerContext, - ) -> Result { + ) -> Result<(RenderFreshness, String), dioxus_ssr::incremental::IncrementalRendererError> { let wrapper = FullstackRenderer { - cfg, + cfg: cfg.clone(), server_context: server_context.clone(), }; match self { Self::Renderer(pool) => { let server_context = Box::new(server_context.clone()); - let mut vdom = VirtualDom::new_with_props(component, props); + let mut renderer = pool.write().unwrap().pop().unwrap_or_else(pre_renderer); - with_server_context(server_context, || { - let _ = vdom.rebuild(); + let (tx, rx) = tokio::sync::oneshot::channel(); + + spawn_blocking(move || { + tokio::runtime::Runtime::new() + .expect("couldn't spawn runtime") + .block_on(async move { + let mut vdom = VirtualDom::new_with_props(component, props); + let mut to = WriteBuffer { buffer: Vec::new() }; + // before polling the future, we need to set the context + let prev_context = + SERVER_CONTEXT.with(|ctx| ctx.replace(server_context)); + // poll the future, which may call server_context() + log::info!("Rebuilding vdom"); + let _ = vdom.rebuild(); + vdom.wait_for_suspense().await; + log::info!("Suspense resolved"); + // after polling the future, we need to restore the context + SERVER_CONTEXT.with(|ctx| ctx.replace(prev_context)); + + if let Err(err) = wrapper.render_before_body(&mut *to) { + let _ = tx.send(Err(err)); + return; + } + if let Err(err) = renderer.render_to(&mut to, &vdom) { + let _ = tx.send(Err( + dioxus_router::prelude::IncrementalRendererError::RenderError( + err, + ), + )); + return; + } + if let Err(err) = wrapper.render_after_body(&mut *to) { + let _ = tx.send(Err(err)); + return; + } + match String::from_utf8(to.buffer) { + Ok(html) => { + let _ = + tx.send(Ok((renderer, RenderFreshness::now(None), html))); + } + Err(err) => { + dioxus_ssr::incremental::IncrementalRendererError::Other( + Box::new(err), + ); + } + } + }); }); - - let mut renderer = pool.pull(pre_renderer); - - // SAFETY: The fullstack renderer will only write UTF-8 to the buffer. - wrapper.render_before_body(&mut **to)?; - renderer.render_to(to, &vdom)?; - wrapper.render_after_body(&mut **to)?; - - Ok(RenderFreshness::now(None)) + let (renderer, freshness, html) = rx.await.unwrap()?; + pool.write().unwrap().push(renderer); + Ok((freshness, html)) } Self::Incremental(pool) => { let mut renderer = - pool.pull(|| incremental_pre_renderer(cfg.incremental.as_ref().unwrap())); - Ok(renderer - .render( - route, - component, - props, - &mut **to, - |vdom| { - let server_context = Box::new(server_context.clone()); - with_server_context(server_context, || { - let _ = vdom.rebuild(); - }); - }, - &wrapper, - ) - .await?) + pool.write().unwrap().pop().unwrap_or_else(|| { + incremental_pre_renderer(cfg.incremental.as_ref().unwrap()) + }); + + let (tx, rx) = tokio::sync::oneshot::channel(); + + let server_context = server_context.clone(); + spawn_blocking(move || { + tokio::runtime::Runtime::new() + .expect("couldn't spawn runtime") + .block_on(async move { + let mut to = WriteBuffer { buffer: Vec::new() }; + match renderer + .render( + route, + component, + props, + &mut *to, + |vdom| { + Box::pin(async move { + // before polling the future, we need to set the context + let prev_context = SERVER_CONTEXT + .with(|ctx| ctx.replace(Box::new(server_context))); + // poll the future, which may call server_context() + log::info!("Rebuilding vdom"); + let _ = vdom.rebuild(); + vdom.wait_for_suspense().await; + log::info!("Suspense resolved"); + // after polling the future, we need to restore the context + SERVER_CONTEXT.with(|ctx| ctx.replace(prev_context)); + }) + }, + &wrapper, + ) + .await + { + Ok(freshness) => { + match String::from_utf8(to.buffer).map_err(|err| { + dioxus_ssr::incremental::IncrementalRendererError::Other( + Box::new(err), + ) + }) { + Ok(html) => { + let _ = tx.send(Ok((freshness, html))); + } + Err(err) => { + let _ = tx.send(Err(err)); + } + } + } + Err(err) => { + let _ = tx.send(Err(err)); + } + } + }) + }); + let (freshness, html) = rx.await.unwrap()?; + + Ok((freshness, html)) } } } @@ -83,18 +167,22 @@ impl SSRState { pub(crate) fn new(cfg: &ServeConfig

) -> Self { if cfg.incremental.is_some() { return Self { - renderers: Arc::new(SsrRendererPool::Incremental(object_pool::Pool::new( - 10, - || incremental_pre_renderer(cfg.incremental.as_ref().unwrap()), - ))), + renderers: Arc::new(SsrRendererPool::Incremental(RwLock::new(vec![ + incremental_pre_renderer(cfg.incremental.as_ref().unwrap()), + incremental_pre_renderer(cfg.incremental.as_ref().unwrap()), + incremental_pre_renderer(cfg.incremental.as_ref().unwrap()), + incremental_pre_renderer(cfg.incremental.as_ref().unwrap()), + ]))), }; } Self { - renderers: Arc::new(SsrRendererPool::Renderer(object_pool::Pool::new( - 10, - pre_renderer, - ))), + renderers: Arc::new(SsrRendererPool::Renderer(RwLock::new(vec![ + pre_renderer(), + pre_renderer(), + pre_renderer(), + pre_renderer(), + ]))), } } @@ -109,31 +197,25 @@ impl SSRState { > + Send + 'a { async move { - let mut html = WriteBuffer { buffer: Vec::new() }; let ServeConfig { app, props, .. } = cfg; - let freshness = self + let (freshness, html) = self .renderers - .render_to(cfg, route, *app, props.clone(), &mut html, server_context) + .render_to(cfg, route, *app, props.clone(), server_context) .await?; - Ok(RenderResponse { - html: String::from_utf8(html.buffer).map_err(|err| { - dioxus_ssr::incremental::IncrementalRendererError::Other(Box::new(err)) - })?, - freshness, - }) + Ok(RenderResponse { html, freshness }) } } } -struct FullstackRenderer<'a, P: Clone + Send + Sync + 'static> { - cfg: &'a ServeConfig

, +struct FullstackRenderer { + cfg: ServeConfig

, server_context: DioxusServerContext, } -impl<'a, P: Clone + Serialize + Send + Sync + 'static> dioxus_ssr::incremental::WrapBody - for FullstackRenderer<'a, P> +impl dioxus_ssr::incremental::WrapBody + for FullstackRenderer

{ fn render_before_body( &self, @@ -258,7 +340,10 @@ where Rt: dioxus_router::prelude::Routable + Send + Sync + Serialize, ::Err: std::fmt::Display, { - let wrapper = FullstackRenderer { cfg }; + let wrapper = FullstackRenderer { + cfg: cfg.clone(), + server_context: Default::default(), + }; let mut renderer = incremental_pre_renderer( cfg.incremental .as_ref() diff --git a/packages/fullstack/src/server_context.rs b/packages/fullstack/src/server_context.rs index 9315434b8..6f3b4b6c9 100644 --- a/packages/fullstack/src/server_context.rs +++ b/packages/fullstack/src/server_context.rs @@ -123,7 +123,7 @@ mod server_fn_impl { } std::thread_local! { - static SERVER_CONTEXT: std::cell::RefCell> = std::cell::RefCell::new(Box::new(DioxusServerContext::default() )); + pub(crate) static SERVER_CONTEXT: std::cell::RefCell> = std::cell::RefCell::new(Box::new(DioxusServerContext::default() )); } /// Get information about the current server request. diff --git a/packages/router/src/incremental.rs b/packages/router/src/incremental.rs index f465f45f6..aad60e27a 100644 --- a/packages/router/src/incremental.rs +++ b/packages/router/src/incremental.rs @@ -1,4 +1,6 @@ //! Exentsions to the incremental renderer to support pre-caching static routes. +use core::pin::Pin; +use std::future::Future; use std::str::FromStr; use dioxus::prelude::*; @@ -47,7 +49,9 @@ where route, &mut tokio::io::sink(), |vdom| { - let _ = vdom.rebuild(); + Box::pin(async move { + let _ = vdom.wait_for_suspense().await; + }) }, wrapper, ) @@ -65,7 +69,12 @@ where } /// Render a route to a writer. -pub async fn render_route( +pub async fn render_route< + R: WrapBody + Send + Sync, + Rt, + W, + F: FnOnce(&mut VirtualDom) -> Pin + '_>>, +>( renderer: &mut IncrementalRenderer, route: Rt, writer: &mut W, diff --git a/packages/ssr/src/fs_cache.rs b/packages/ssr/src/fs_cache.rs index efaa52b80..0abf9c319 100644 --- a/packages/ssr/src/fs_cache.rs +++ b/packages/ssr/src/fs_cache.rs @@ -1,14 +1,11 @@ #![allow(non_snake_case)] - - use std::{ ops::{Deref, DerefMut}, - path::{PathBuf}, - time::{Duration}, + path::PathBuf, + time::Duration, }; - /// Information about the freshness of a rendered response #[derive(Debug, Clone, Copy)] pub struct RenderFreshness { diff --git a/packages/ssr/src/incremental.rs b/packages/ssr/src/incremental.rs index 00fd3e9e2..fcecaacc5 100644 --- a/packages/ssr/src/incremental.rs +++ b/packages/ssr/src/incremental.rs @@ -6,10 +6,12 @@ use crate::fs_cache::ValidCachedPath; use dioxus_core::{Element, Scope, VirtualDom}; use rustc_hash::FxHasher; use std::{ + future::Future, hash::BuildHasherDefault, io::Write, ops::{Deref, DerefMut}, - path::{PathBuf}, + path::PathBuf, + pin::Pin, time::{Duration, SystemTime}, }; use tokio::io::{AsyncWrite, AsyncWriteExt, BufReader}; @@ -67,36 +69,29 @@ impl IncrementalRenderer { self.invalidate_after.is_some() } - fn render_and_cache<'a, P: 'static, R: WrapBody + Send + Sync>( + async fn render_and_cache<'a, P: 'static, R: WrapBody + Send + Sync>( &'a mut self, route: String, comp: fn(Scope

) -> Element, props: P, output: &'a mut (impl AsyncWrite + Unpin + Send), - rebuild_with: impl FnOnce(&mut VirtualDom), + rebuild_with: impl FnOnce(&mut VirtualDom) -> Pin + '_>>, renderer: &'a R, - ) -> impl std::future::Future> + 'a + Send - { + ) -> Result { let mut html_buffer = WriteBuffer { buffer: Vec::new() }; - let result_1; - let result2; { let mut vdom = VirtualDom::new_with_props(comp, props); - rebuild_with(&mut vdom); + rebuild_with(&mut vdom).await; - result_1 = renderer.render_before_body(&mut *html_buffer); - result2 = self.ssr_renderer.render_to(&mut html_buffer, &vdom); + renderer.render_before_body(&mut *html_buffer)?; + self.ssr_renderer.render_to(&mut html_buffer, &vdom)?; } - async move { - result_1?; - result2?; - renderer.render_after_body(&mut *html_buffer)?; - let html_buffer = html_buffer.buffer; + renderer.render_after_body(&mut *html_buffer)?; + let html_buffer = html_buffer.buffer; - output.write_all(&html_buffer).await?; + output.write_all(&html_buffer).await?; - self.add_to_cache(route, html_buffer) - } + self.add_to_cache(route, html_buffer) } fn add_to_cache( @@ -178,7 +173,7 @@ impl IncrementalRenderer { component: fn(Scope

) -> Element, props: P, output: &mut (impl AsyncWrite + Unpin + std::marker::Send), - rebuild_with: impl FnOnce(&mut VirtualDom), + rebuild_with: impl FnOnce(&mut VirtualDom) -> Pin + '_>>, renderer: &R, ) -> Result { // check if this route is cached From 4ab30c9e7678d6635e67d13918831e3772b42bb8 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 17 Jul 2023 16:50:30 -0700 Subject: [PATCH 5/9] remove cargo cache --- packages/fullstack/examples/axum-hello-world/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/fullstack/examples/axum-hello-world/Cargo.toml b/packages/fullstack/examples/axum-hello-world/Cargo.toml index 3746e0b07..b98d9f1a1 100644 --- a/packages/fullstack/examples/axum-hello-world/Cargo.toml +++ b/packages/fullstack/examples/axum-hello-world/Cargo.toml @@ -18,7 +18,6 @@ tower-http = { version = "0.4.1", features = ["auth"] } simple_logger = "4.2.0" wasm-logger = "0.2.0" log.workspace = true -cargo-cache = "0.8.3" [features] default = [] From 8e54a89a742987753d6440f3e7f309d305e426db Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 17 Jul 2023 17:23:15 -0700 Subject: [PATCH 6/9] fix use_server_future --- .../examples/axum-hello-world/Cargo.toml | 1 + .../examples/axum-hello-world/src/main.rs | 21 ++++----- .../examples/salvo-hello-world/Cargo.toml | 4 ++ .../examples/salvo-hello-world/src/main.rs | 46 +++++++++++++------ .../examples/warp-hello-world/Cargo.toml | 4 ++ .../examples/warp-hello-world/src/main.rs | 46 +++++++++++++------ packages/fullstack/src/hooks/server_future.rs | 46 +++++++++++-------- .../fullstack/src/html_storage/deserialize.rs | 35 +++++++++++--- packages/fullstack/src/render.rs | 3 +- 9 files changed, 141 insertions(+), 65 deletions(-) diff --git a/packages/fullstack/examples/axum-hello-world/Cargo.toml b/packages/fullstack/examples/axum-hello-world/Cargo.toml index b98d9f1a1..502d75bae 100644 --- a/packages/fullstack/examples/axum-hello-world/Cargo.toml +++ b/packages/fullstack/examples/axum-hello-world/Cargo.toml @@ -18,6 +18,7 @@ tower-http = { version = "0.4.1", features = ["auth"] } simple_logger = "4.2.0" wasm-logger = "0.2.0" log.workspace = true +reqwest = "0.11.18" [features] default = [] diff --git a/packages/fullstack/examples/axum-hello-world/src/main.rs b/packages/fullstack/examples/axum-hello-world/src/main.rs index 09bdcf3ce..cabe4b00e 100644 --- a/packages/fullstack/examples/axum-hello-world/src/main.rs +++ b/packages/fullstack/examples/axum-hello-world/src/main.rs @@ -23,16 +23,13 @@ fn app(cx: Scope) -> Element { fn Child(cx: Scope) -> Element { let state = use_server_future(cx, (), |()| async move { - #[cfg(not(feature = "ssr"))] - panic!(); - #[cfg(feature = "ssr")] - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - return 1; - })?; - - log::info!("running child"); - let state = state.value(); - log::info!("child state: {:?}", state); + loop { + if let Ok(res) = get_server_data().await { + break res; + } + } + })? + .value(); let mut count = use_state(cx, || 0); let text = use_state(cx, || "...".to_string()); @@ -72,12 +69,12 @@ async fn post_server_data(data: String) -> Result<(), ServerFnError> { #[server(GetServerData)] async fn get_server_data() -> Result { - Ok("Hello from the server!".to_string()) + Ok(reqwest::get("https://httpbin.org/ip").await?.text().await?) } fn main() { #[cfg(feature = "web")] - wasm_logger::init(wasm_logger::Config::default()); + wasm_logger::init(wasm_logger::Config::new(log::Level::Trace)); #[cfg(feature = "ssr")] simple_logger::SimpleLogger::new().init().unwrap(); diff --git a/packages/fullstack/examples/salvo-hello-world/Cargo.toml b/packages/fullstack/examples/salvo-hello-world/Cargo.toml index b85651f06..977ad7843 100644 --- a/packages/fullstack/examples/salvo-hello-world/Cargo.toml +++ b/packages/fullstack/examples/salvo-hello-world/Cargo.toml @@ -14,6 +14,10 @@ tokio = { workspace = true, features = ["full"], optional = true } serde = "1.0.159" salvo = { version = "0.37.9", optional = true } execute = "0.2.12" +reqwest = "0.11.18" +simple_logger = "4.2.0" +log.workspace = true +wasm-logger = "0.2.0" [features] default = [] diff --git a/packages/fullstack/examples/salvo-hello-world/src/main.rs b/packages/fullstack/examples/salvo-hello-world/src/main.rs index 9e5649efc..765b1836a 100644 --- a/packages/fullstack/examples/salvo-hello-world/src/main.rs +++ b/packages/fullstack/examples/salvo-hello-world/src/main.rs @@ -7,25 +7,37 @@ #![allow(non_snake_case, unused)] use dioxus::prelude::*; -use dioxus_fullstack::prelude::*; +use dioxus_fullstack::{launch, prelude::*}; use serde::{Deserialize, Serialize}; -fn main() { - launch!(@([127, 0, 0, 1], 8080), app, (AppProps { count: 5 }), { - incremental: IncrementalRendererConfig::default().invalidate_after(std::time::Duration::from_secs(120)), - }); -} - #[derive(Props, PartialEq, Debug, Default, Serialize, Deserialize, Clone)] struct AppProps { count: i32, } fn app(cx: Scope) -> Element { - let mut count = use_state(cx, || cx.props.count); + render! { + Child {} + } +} + +fn Child(cx: Scope) -> Element { + let state = use_server_future(cx, (), |()| async move { + loop { + if let Ok(res) = get_server_data().await { + break res; + } + } + })? + .value(); + + let mut count = use_state(cx, || 0); let text = use_state(cx, || "...".to_string()); cx.render(rsx! { + div { + "Server state: {state}" + } h1 { "High-Five counter: {count}" } button { onclick: move |_| count += 1, "Up high!" } button { onclick: move |_| count -= 1, "Down low!" } @@ -40,7 +52,7 @@ fn app(cx: Scope) -> Element { } } }, - "Run a server function" + "Run a server function!" } "Server said: {text}" }) @@ -48,15 +60,23 @@ fn app(cx: Scope) -> Element { #[server(PostServerData)] async fn post_server_data(data: String) -> Result<(), ServerFnError> { - // The server context contains information about the current request and allows you to modify the response. - let cx = server_context(); println!("Server received: {}", data); - println!("Request parts are {:?}", cx.request_parts()); Ok(()) } #[server(GetServerData)] async fn get_server_data() -> Result { - Ok("Hello from the server!".to_string()) + Ok(reqwest::get("https://httpbin.org/ip").await?.text().await?) +} + +fn main() { + #[cfg(feature = "web")] + wasm_logger::init(wasm_logger::Config::new(log::Level::Trace)); + #[cfg(feature = "ssr")] + simple_logger::SimpleLogger::new().init().unwrap(); + + launch!(@([127, 0, 0, 1], 8080), app, { + serve_cfg: ServeConfigBuilder::new(app, AppProps { count: 0 }), + }); } diff --git a/packages/fullstack/examples/warp-hello-world/Cargo.toml b/packages/fullstack/examples/warp-hello-world/Cargo.toml index 3c21372a9..d9e82b4c4 100644 --- a/packages/fullstack/examples/warp-hello-world/Cargo.toml +++ b/packages/fullstack/examples/warp-hello-world/Cargo.toml @@ -14,6 +14,10 @@ tokio = { workspace = true, features = ["full"], optional = true } serde = "1.0.159" warp = { version = "0.3.3", optional = true } execute = "0.2.12" +reqwest = "0.11.18" +simple_logger = "4.2.0" +log.workspace = true +wasm-logger = "0.2.0" [features] default = [] diff --git a/packages/fullstack/examples/warp-hello-world/src/main.rs b/packages/fullstack/examples/warp-hello-world/src/main.rs index 9e5649efc..765b1836a 100644 --- a/packages/fullstack/examples/warp-hello-world/src/main.rs +++ b/packages/fullstack/examples/warp-hello-world/src/main.rs @@ -7,25 +7,37 @@ #![allow(non_snake_case, unused)] use dioxus::prelude::*; -use dioxus_fullstack::prelude::*; +use dioxus_fullstack::{launch, prelude::*}; use serde::{Deserialize, Serialize}; -fn main() { - launch!(@([127, 0, 0, 1], 8080), app, (AppProps { count: 5 }), { - incremental: IncrementalRendererConfig::default().invalidate_after(std::time::Duration::from_secs(120)), - }); -} - #[derive(Props, PartialEq, Debug, Default, Serialize, Deserialize, Clone)] struct AppProps { count: i32, } fn app(cx: Scope) -> Element { - let mut count = use_state(cx, || cx.props.count); + render! { + Child {} + } +} + +fn Child(cx: Scope) -> Element { + let state = use_server_future(cx, (), |()| async move { + loop { + if let Ok(res) = get_server_data().await { + break res; + } + } + })? + .value(); + + let mut count = use_state(cx, || 0); let text = use_state(cx, || "...".to_string()); cx.render(rsx! { + div { + "Server state: {state}" + } h1 { "High-Five counter: {count}" } button { onclick: move |_| count += 1, "Up high!" } button { onclick: move |_| count -= 1, "Down low!" } @@ -40,7 +52,7 @@ fn app(cx: Scope) -> Element { } } }, - "Run a server function" + "Run a server function!" } "Server said: {text}" }) @@ -48,15 +60,23 @@ fn app(cx: Scope) -> Element { #[server(PostServerData)] async fn post_server_data(data: String) -> Result<(), ServerFnError> { - // The server context contains information about the current request and allows you to modify the response. - let cx = server_context(); println!("Server received: {}", data); - println!("Request parts are {:?}", cx.request_parts()); Ok(()) } #[server(GetServerData)] async fn get_server_data() -> Result { - Ok("Hello from the server!".to_string()) + Ok(reqwest::get("https://httpbin.org/ip").await?.text().await?) +} + +fn main() { + #[cfg(feature = "web")] + wasm_logger::init(wasm_logger::Config::new(log::Level::Trace)); + #[cfg(feature = "ssr")] + simple_logger::SimpleLogger::new().init().unwrap(); + + launch!(@([127, 0, 0, 1], 8080), app, { + serve_cfg: ServeConfigBuilder::new(app, AppProps { count: 0 }), + }); } diff --git a/packages/fullstack/src/hooks/server_future.rs b/packages/fullstack/src/hooks/server_future.rs index 2e3789ab7..9a13df235 100644 --- a/packages/fullstack/src/hooks/server_future.rs +++ b/packages/fullstack/src/hooks/server_future.rs @@ -4,6 +4,7 @@ use std::any::Any; use std::cell::Cell; use std::cell::Ref; use std::cell::RefCell; +use std::fmt::Debug; use std::future::Future; use std::rc::Rc; use std::sync::Arc; @@ -27,7 +28,7 @@ pub fn use_server_future( future: impl FnOnce(D::Out) -> F, ) -> Option<&UseServerFuture> where - T: 'static + Serialize + DeserializeOwned, + T: 'static + Serialize + DeserializeOwned + Debug, F: Future + 'static, D: UseFutureDep, { @@ -39,7 +40,24 @@ where dependencies: Vec::new(), }); - let first_run = { state.value.borrow().as_ref().is_none() }; + let first_run = { state.value.borrow().as_ref().is_none() && state.task.get().is_none() }; + + #[cfg(not(feature = "ssr"))] + { + if first_run { + match crate::html_storage::deserialize::take_server_data() { + Some(data) => { + log::trace!("Loaded {data:?} from server"); + *state.value.borrow_mut() = Some(Box::new(data)); + state.needs_regen.set(false); + return Some(state); + } + None => { + log::trace!("Failed to load from server... running future"); + } + }; + } + } if dependencies.clone().apply(&mut state.dependencies) || state.needs_regen.get() { // We don't need regen anymore @@ -70,10 +88,7 @@ where } #[cfg(not(feature = "ssr"))] { - data = match crate::html_storage::deserialize::take_server_data() { - Some(data) => data, - None => fut.await, - }; + data = fut.await; } *value.borrow_mut() = Some(Box::new(data)); @@ -82,20 +97,17 @@ where } if first_run { - log::trace!("Suspending first run of use_server_future"); - cx.suspend(); + #[cfg(feature = "ssr")] + { + log::trace!("Suspending first run of use_server_future"); + cx.suspend(); + } None } else { Some(state) } } -pub enum FutureState<'a, T> { - Pending, - Complete(&'a T), - Regenerating(&'a T), // the old value -} - pub struct UseServerFuture { update: Arc, needs_regen: Cell, @@ -104,12 +116,6 @@ pub struct UseServerFuture { value: Rc>>>, } -pub enum UseFutureState<'a, T> { - Pending, - Complete(&'a T), - Reloading(&'a T), -} - impl UseServerFuture { /// Restart the future with new dependencies. /// diff --git a/packages/fullstack/src/html_storage/deserialize.rs b/packages/fullstack/src/html_storage/deserialize.rs index 3a0fc51df..822b45076 100644 --- a/packages/fullstack/src/html_storage/deserialize.rs +++ b/packages/fullstack/src/html_storage/deserialize.rs @@ -7,19 +7,42 @@ use super::HTMLDataCursor; #[allow(unused)] pub(crate) fn serde_from_bytes(string: &[u8]) -> Option { - let decompressed = STANDARD.decode(string).ok()?; + let decompressed = match STANDARD.decode(string) { + Ok(bytes) => bytes, + Err(err) => { + log::error!("Failed to decode base64: {}", err); + return None; + } + }; - postcard::from_bytes(&decompressed).ok() + match postcard::from_bytes(&decompressed) { + Ok(data) => Some(data), + Err(err) => { + log::error!("Failed to deserialize: {}", err); + None + } + } } static SERVER_DATA: once_cell::sync::Lazy> = once_cell::sync::Lazy::new(|| { #[cfg(target_arch = "wasm32")] { - let attribute = web_sys::window()? - .document()? - .get_element_by_id("dioxus-storage-data")? - .get_attribute("data-serialized")?; + let window = web_sys::window()?.document()?; + let element = match window.get_element_by_id("dioxus-storage-data") { + Some(element) => element, + None => { + log::error!("Failed to get element by id: dioxus-storage-data"); + return None; + } + }; + let attribute = match element.get_attribute("data-serialized") { + Some(attribute) => attribute, + None => { + log::error!("Failed to get attribute: data-serialized"); + return None; + } + }; let data: super::HTMLData = serde_from_bytes(attribute.as_bytes())?; diff --git a/packages/fullstack/src/render.rs b/packages/fullstack/src/render.rs index c6a13c347..9699a5eb3 100644 --- a/packages/fullstack/src/render.rs +++ b/packages/fullstack/src/render.rs @@ -12,7 +12,7 @@ use serde::Serialize; use std::sync::RwLock; use tokio::task::spawn_blocking; -use crate::{prelude::*, server_context::with_server_context}; +use crate::prelude::*; use dioxus::prelude::*; enum SsrRendererPool { @@ -300,6 +300,7 @@ impl dioxus_ssr::incremental::Wrap } /// A rendered response from the server. +#[derive(Debug)] pub struct RenderResponse { pub(crate) html: String, pub(crate) freshness: RenderFreshness, From f3d1600822cec2b28c335fc2109e77097a32699d Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 18 Jul 2023 10:40:09 -0700 Subject: [PATCH 7/9] fix clippy --- packages/fullstack/src/hooks/server_cached.rs | 2 +- packages/fullstack/src/html_storage/mod.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/fullstack/src/hooks/server_cached.rs b/packages/fullstack/src/hooks/server_cached.rs index e28783b77..80bb93008 100644 --- a/packages/fullstack/src/hooks/server_cached.rs +++ b/packages/fullstack/src/hooks/server_cached.rs @@ -29,6 +29,6 @@ pub fn server_cached(server_fn: impl } #[cfg(not(feature = "ssr"))] { - crate::html_storage::deserialize::take_server_data().unwrap_or_else(|| server_fn()) + crate::html_storage::deserialize::take_server_data().unwrap_or_else(server_fn) } } diff --git a/packages/fullstack/src/html_storage/mod.rs b/packages/fullstack/src/html_storage/mod.rs index f0fbca127..2d64953ae 100644 --- a/packages/fullstack/src/html_storage/mod.rs +++ b/packages/fullstack/src/html_storage/mod.rs @@ -45,7 +45,7 @@ impl HTMLDataCursor { } let mut cursor = &self.data[current]; self.index.fetch_add(1, std::sync::atomic::Ordering::SeqCst); - match postcard::from_bytes(&mut cursor) { + match postcard::from_bytes(cursor) { Ok(x) => Some(x), Err(e) => { log::error!("Error deserializing data: {:?}", e); From 7e2ef1260fa9fdb3dc434db6ca49d479b2d94380 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 19 Jul 2023 14:19:03 -0700 Subject: [PATCH 8/9] Merge branch 'master' into use-server-function --- .github/workflows/docs stable.yml | 4 + .github/workflows/docs.yml | 4 + .github/workflows/macos.yml | 4 + .github/workflows/main.yml | 4 + .github/workflows/miri.yml | 11 +- .github/workflows/playwright.yml | 32 ++++-- .github/workflows/windows.yml | 4 + .../en/__unused/advanced-guides/testing.md | 2 +- .../src/en/contributing/walkthrough_readme.md | 64 ++++++----- .../guide/src/en/fullstack/getting_started.md | 4 +- .../src/en/fullstack/server_functions.md | 2 +- .../src/en/getting_started/hot_reload.md | 2 +- docs/guide/src/en/getting_started/web.md | 2 +- docs/guide/src/en/interactivity/hooks.md | 35 ++++-- .../src/pt-br/getting_started/hot_reload.md | 2 +- docs/router/examples/catch_all.rs | 2 +- docs/router/examples/catch_all_segments.rs | 2 +- docs/router/examples/dynamic_route.rs | 2 +- docs/router/examples/full_example.rs | 2 +- docs/router/examples/links.rs | 2 +- docs/router/examples/navigator.rs | 2 +- docs/router/examples/nested_routes.rs | 2 +- examples/PWA-example/README.md | 6 +- examples/file_upload.rs | 27 ++++- examples/router.rs | 2 +- examples/tailwind/README.md | 2 +- notes/FAQ.md | 2 +- packages/cli/Cargo.toml | 7 -- packages/cli/Dioxus.toml | 4 +- packages/cli/README.md | 4 +- packages/cli/docs/src/cmd/README.md | 8 +- packages/cli/docs/src/cmd/build.md | 18 +-- packages/cli/docs/src/cmd/clean.md | 12 +- packages/cli/docs/src/cmd/serve.md | 20 ++-- packages/cli/docs/src/cmd/translate.md | 14 +-- packages/cli/docs/src/configure.md | 4 +- packages/cli/docs/src/creating.md | 8 +- packages/cli/docs/src/plugin/README.md | 4 +- packages/cli/src/assets/dioxus.toml | 2 +- packages/cli/src/builder.rs | 4 +- packages/cli/src/cli/autoformat.rs | 8 +- packages/cli/src/cli/plugin.rs | 2 +- packages/core/src/create.rs | 5 +- packages/core/src/diff.rs | 18 +-- packages/core/src/nodes.rs | 105 +----------------- packages/core/src/scope_arena.rs | 2 +- packages/core/src/scopes.rs | 4 +- packages/core/src/virtual_dom.rs | 3 + packages/core/tests/task.rs | 13 ++- packages/desktop/src/file_upload.rs | 64 +++++++---- packages/desktop/src/protocol.rs | 2 +- packages/dioxus-tui/examples/colorpicker.rs | 2 +- .../fullstack/examples/axum-auth/src/main.rs | 2 +- .../examples/axum-hello-world/src/main.rs | 2 +- .../examples/axum-router/src/main.rs | 2 +- .../examples/salvo-hello-world/src/main.rs | 2 +- .../examples/static-hydrated/src/main.rs | 2 +- .../examples/warp-hello-world/src/main.rs | 2 +- packages/html/src/elements.rs | 1 + packages/html/src/events/form.rs | 7 +- packages/interpreter/src/common.js | 1 + .../interpreter/src/sledgehammer_bindings.rs | 1 + .../liveview/src/adapters/axum_adapter.rs | 7 +- .../liveview/src/adapters/salvo_adapter.rs | 10 +- .../liveview/src/adapters/warp_adapter.rs | 9 +- packages/liveview/src/pool.rs | 26 +++-- packages/rink/src/widgets/button.rs | 16 ++- packages/rink/src/widgets/checkbox.rs | 20 +++- packages/rink/src/widgets/input.rs | 4 +- packages/rink/src/widgets/slider.rs | 20 +++- packages/rink/src/widgets/text_like.rs | 24 +++- packages/router-macro/src/lib.rs | 18 +-- packages/router-macro/src/route.rs | 6 +- packages/router-macro/src/segment.rs | 4 +- packages/signals/src/lib.rs | 8 +- packages/web/src/rehydrate.rs | 2 +- playwright-tests/fullstack/src/main.rs | 2 +- .../package-lock.json | 20 ++-- package.json => playwright-tests/package.json | 2 +- playwright-tests/playwright-report/index.html | 62 +++++++++++ .../playwright.config.js | 10 +- 81 files changed, 483 insertions(+), 375 deletions(-) rename package-lock.json => playwright-tests/package-lock.json (76%) rename package.json => playwright-tests/package.json (90%) create mode 100644 playwright-tests/playwright-report/index.html rename playwright.config.js => playwright-tests/playwright.config.js (88%) diff --git a/.github/workflows/docs stable.yml b/.github/workflows/docs stable.yml index a2d4e2c07..069d4308a 100644 --- a/.github/workflows/docs stable.yml +++ b/.github/workflows/docs stable.yml @@ -3,6 +3,10 @@ name: docs stable on: workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: build-deploy: runs-on: ubuntu-latest diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index dafe0384d..3a452e770 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -8,6 +8,10 @@ on: branches: - master +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: build-deploy: runs-on: ubuntu-latest diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index eef04d521..471bdd715 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -24,6 +24,10 @@ on: - lib.rs - Cargo.toml +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: test: if: github.event.pull_request.draft == false diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b37e63aa4..a95af8574 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,6 +27,10 @@ on: - lib.rs - Cargo.toml +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: check: if: github.event.pull_request.draft == false diff --git a/.github/workflows/miri.yml b/.github/workflows/miri.yml index 73b38dec9..e8aab0732 100644 --- a/.github/workflows/miri.yml +++ b/.github/workflows/miri.yml @@ -6,6 +6,13 @@ on: branches: - 'auto' - 'try' + paths: + - packages/** + - examples/** + - src/** + - .github/** + - lib.rs + - Cargo.toml pull_request: types: [opened, synchronize, reopened, ready_for_review] branches: @@ -31,7 +38,9 @@ env: # - tokio-stream/Cargo.toml # rust_min: 1.49.0 - +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: test: diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 84891f403..41fa92fa7 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -4,22 +4,25 @@ on: branches: [ main, master ] pull_request: branches: [ main, master ] +defaults: + run: + working-directory: ./playwright-tests + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: test: if: github.event.pull_request.draft == false timeout-minutes: 60 runs-on: ubuntu-20.04 steps: + # Do our best to cache the toolchain and node install steps - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: 16 - - name: Install dependencies - run: npm ci - - name: Install Playwright - run: npm install -D @playwright/test - - name: Install Playwright Browsers - run: npx playwright install --with-deps - name: Install Rust uses: actions-rs/toolchain@v1 with: @@ -29,11 +32,18 @@ jobs: - uses: Swatinem/rust-cache@v2 - name: Install WASM toolchain run: rustup target add wasm32-unknown-unknown - - name: Install Dioxus CLI - uses: actions-rs/cargo@v1 - with: - command: install - args: --path packages/cli + - name: Install dependencies + run: npm ci + - name: Install Playwright + run: npm install -D @playwright/test + - name: Install Playwright Browsers + run: npx playwright install --with-deps + # Cache the CLI by using cargo run internally + # - name: Install Dioxus CLI + # uses: actions-rs/cargo@v1 + # with: + # command: install + # args: --path packages/cli - name: Run Playwright tests run: npx playwright test - uses: actions/upload-artifact@v3 diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 368df1630..f5792cf1e 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -24,6 +24,10 @@ on: - lib.rs - Cargo.toml +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: test: if: github.event.pull_request.draft == false diff --git a/docs/guide/src/en/__unused/advanced-guides/testing.md b/docs/guide/src/en/__unused/advanced-guides/testing.md index 87e72309c..405c929a9 100644 --- a/docs/guide/src/en/__unused/advanced-guides/testing.md +++ b/docs/guide/src/en/__unused/advanced-guides/testing.md @@ -21,7 +21,7 @@ fn runs_in_browser() { Then, when you run ```console -dioxus test --chrome +dx test --chrome ``` Dioxus will build and test your code using the Chrome browser as a harness. diff --git a/docs/guide/src/en/contributing/walkthrough_readme.md b/docs/guide/src/en/contributing/walkthrough_readme.md index 5334630c9..5be9d4b5a 100644 --- a/docs/guide/src/en/contributing/walkthrough_readme.md +++ b/docs/guide/src/en/contributing/walkthrough_readme.md @@ -14,13 +14,13 @@ We start will a hello world program. This program renders a desktop app with the ## The rsx! Macro -Before the Rust compiler runs the program, it will expand all macros. Here is what the hello world example looks like expanded: +Before the Rust compiler runs the program, it will expand all [macros](https://doc.rust-lang.org/reference/procedural-macros.html). Here is what the hello world example looks like expanded: ```rust, no_run {{#include ../../../examples/readme_expanded.rs}} ``` -The rsx macro separates the static parts of the rsx (the template) and the dynamic parts (the dynamic_nodes and dynamic_attributes). +The rsx macro separates the static parts of the rsx (the template) and the dynamic parts (the [dynamic_nodes](https://docs.rs/dioxus-core/0.3.2/dioxus_core/prelude/struct.VNode.html#structfield.dynamic_nodes) and [dynamic_attributes](https://docs.rs/dioxus-core/0.3.2/dioxus_core/prelude/struct.VNode.html#structfield.dynamic_attrs)). The static template only contains the parts of the rsx that cannot change at runtime with holes for the dynamic parts: @@ -32,17 +32,17 @@ The dynamic_nodes and dynamic_attributes are the parts of the rsx that can chang ## Launching the App -The app is launched by calling the `launch` function with the root component. Internally, this function will create a new web view using [wry](https://docs.rs/wry/latest/wry/) and create a virtual dom with the root component. This guide will not explain the renderer in-depth, but you can read more about it in the [custom renderer](/guide/custom-renderer) section. +The app is launched by calling the `launch` function with the root component. Internally, this function will create a new web view using [wry](https://docs.rs/wry/latest/wry/) and create a virtual dom with the root component (`fn app()` in the readme example). This guide will not explain the renderer in-depth, but you can read more about it in the [custom renderer](/guide/custom-renderer) section. ## The Virtual DOM -Before we dive into the initial render in the virtual dom, we need to discuss what the virtual dom is. The virtual dom is a representation of the dom that is used to diff the current dom from the new dom. This diff is then used to create a list of mutations that need to be applied to the dom. +Before we dive into the initial render in the virtual DOM, we need to discuss what the virtual DOM is. The virtual DOM is a representation of the DOM that is used to diff the current DOM from the new DOM. This diff is then used to create a list of mutations that need to be applied to the DOM to bring it into sync with the virtual DOM. -The Virtual Dom roughly looks like this: +The Virtual DOM roughly looks like this: ```rust, no_run pub struct VirtualDom { - // All the templates that have been created or set durring hot reloading + // All the templates that have been created or set during hot reloading pub(crate) templates: FxHashMap>>, // A slab of all the scopes that have been created @@ -63,64 +63,74 @@ pub struct VirtualDom { ``` > What is a [slab](https://docs.rs/slab/latest/slab/)? +> > A slab acts like a hashmap with integer keys if you don't care about the value of the keys. It is internally backed by a dense vector which makes it more efficient than a hashmap. When you insert a value into a slab, it returns an integer key that you can use to retrieve the value later. > How does Dioxus use slabs? -> Dioxus uses "synchronized slabs" to communicate between the renderer and the VDOM. When an node is created in the Virtual Dom, a ElementId is passed along with the mutation to the renderer to identify the node. These ids are used by the Virtual Dom to reference that nodes in future mutations like setting an attribute on a node or removing a node. -> When the renderer sends an event to the Virtual Dom, it sends the ElementId of the node that the event was triggered on. The Virtual Dom uses this id to find the node in the slab and then run the necessary event handlers. +> +> Dioxus uses "synchronized slabs" to communicate between the renderer and the VDOM. When a node is created in the Virtual DOM, an (elementId, mutation) pair is passed to the renderer to identify that node, which the renderer will then render in actual DOM. These ids are also used by the Virtual Dom to reference that node in future mutations, like setting an attribute on a node or removing a node. When the renderer sends an event to the Virtual Dom, it sends the ElementId of the node that the event was triggered on. The Virtual DOM uses this id to find that node in the slab and then run the necessary event handlers. -The virtual dom is a tree of scopes. A new scope is created for every component when it is first rendered and recycled when the component is unmounted. +The virtual DOM is a tree of scopes. A new `Scope` is created for every component when it is first rendered and recycled when the component is unmounted. Scopes serve three main purposes: 1. They store the state of hooks used by the component -2. They store the state for the context API -3. They store the current and previous VNode that was rendered for diffing +2. They store the state for the context API (for example: using + [use_shared_state_provider](https://docs.rs/dioxus/latest/dioxus/prelude/fn.use_shared_state_provider.html)). +3. They store the current and previous versions of the `VNode` that was rendered, so they can be + diffed to generate the set of mutations needed to re-render it. ### The Initial Render The root scope is created and rebuilt: 1. The root component is run -2. The root component returns a VNode -3. Mutations for the VNode are created and added to the mutation list (this may involve creating new child components) -4. The VNode is stored in the root scope +2. The root component returns a `VNode` +3. Mutations for this `VNode` are created and added to the mutation list (this may involve creating new child components) +4. The `VNode` is stored in the root's `Scope`. -After the root scope is built, the mutations are sent to the renderer to be applied to the dom. +After the root's `Scope` is built, all generated mutations are sent to the renderer, which applies them to the DOM. -After the initial render, the root scope looks like this: +After the initial render, the root `Scope` looks like this: [![](https://mermaid.ink/img/pako:eNqtVE1P4zAQ_SuzPrWikRpWXCLtBRDisItWsOxhCaqM7RKricdyJrQV8N93QtvQNCkfEnOynydv3nxkHoVCbUQipjnOVSYDwc_L1AFbWd3dB-kzuEQkuFLoDUwDFkCZAek9nGDh0RlHK__atA1GkUUHf45f0YbppAqB_aOzIAvz-t7-chN_Y-1bw1WSJKsglIu2w9tktWXxIIuHURT5XCqTYa5NmDguw2R8c5MKq2GcgF46WTB_jafi9rZL0yi5q4jQTSrf9altO4okCn1Ratwyz55Qxuku2ITlTMgs6HCQimsPmb3PvqVi-L5gjXP3QcnxWnL8JZLrwGvR31n0KV-Bx6-r-oVkT_-3G1S-NQLbk9i8rj7udP2cixed2QcDCitHJiQw7ub3EVlNecrPjudG2-6soFO5VbMECmR9T5OnlUY4-AFxfw9aTFst3McU9TK1Otm6NEn_DubBYlX2_dglLXOz48FgwJmJ5lZTlhz6xWgNaFnyDgpymcARHO0W2a9J_l5w2wYXvHuGPcqaQ-rESBQmFNJq3nCPNZoK3l4sUSR81DLMUpG6Z_aTFeHV0imRUKjMSFReSzKnVnKGhUimMi8ZNdoShl-rlfmyOUfCS_cPcePz_B_Wl4pc?type=png)](https://mermaid.live/edit#pako:eNqtVE1P4zAQ_SuzPrWikRpWXCLtBRDisItWsOxhCaqM7RKricdyJrQV8N93QtvQNCkfEnOynydv3nxkHoVCbUQipjnOVSYDwc_L1AFbWd3dB-kzuEQkuFLoDUwDFkCZAek9nGDh0RlHK__atA1GkUUHf45f0YbppAqB_aOzIAvz-t7-chN_Y-1bw1WSJKsglIu2w9tktWXxIIuHURT5XCqTYa5NmDguw2R8c5MKq2GcgF46WTB_jafi9rZL0yi5q4jQTSrf9altO4okCn1Ratwyz55Qxuku2ITlTMgs6HCQimsPmb3PvqVi-L5gjXP3QcnxWnL8JZLrwGvR31n0KV-Bx6-r-oVkT_-3G1S-NQLbk9i8rj7udP2cixed2QcDCitHJiQw7ub3EVlNecrPjudG2-6soFO5VbMECmR9T5OnlUY4-AFxfw9aTFst3McU9TK1Otm6NEn_DubBYlX2_dglLXOz48FgwJmJ5lZTlhz6xWgNaFnyDgpymcARHO0W2a9J_l5w2wYXvHuGPcqaQ-rESBQmFNJq3nCPNZoK3l4sUSR81DLMUpG6Z_aTFeHV0imRUKjMSFReSzKnVnKGhUimMi8ZNdoShl-rlfmyOUfCS_cPcePz_B_Wl4pc) ### Waiting for Events -The Virtual Dom will only ever rerender a scope if it is marked as dirty. Each hook is responsible for marking the scope as dirty if the state has changed. Hooks can mark a scope as dirty by sending a message to the Virtual Dom's channel. +The Virtual DOM will only ever re-render a `Scope` if it is marked as dirty. Each hook is responsible for marking the `Scope` as dirty if the state has changed. Hooks can mark a scope as dirty by sending a message to the Virtual Dom's channel. You can see the [implementations](https://github.com/DioxusLabs/dioxus/tree/master/packages/hooks) for the hooks dioxus includes by default on how this is done. Calling `needs_update()` on a hook will also cause it to mark its scope as dirty. There are generally two ways a scope is marked as dirty: -1. The renderer triggers an event: This causes an event listener to be called if needed which may mark a component as dirty -2. The renderer calls wait for work: This polls futures which may mark a component as dirty +1. The renderer triggers an event: An event listener on this event may be called, which may mark a + component as dirty, if processing the event resulted in any generated any mutations. +2. The renderer calls + [`wait_for_work`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.VirtualDom.html#method.wait_for_work): + This polls dioxus internal future queue. One of these futures may mark a component as dirty. -Once at least one scope is marked as dirty, the renderer can call `render_with_deadline` to diff the dirty scopes. +Once at least one `Scope` is marked as dirty, the renderer can call [`render_with_deadline`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.VirtualDom.html#method.render_with_deadline) to diff the dirty scopes. ### Diffing Scopes -If the user clicked the "up high" button, the root scope would be marked as dirty by the use_state hook. Once the desktop renderer calls `render_with_deadline`, the root scope would be diffed. +When a user clicks the "up high" button, the root `Scope` will be marked as dirty by the `use_state` hook. The desktop renderer will then call `render_with_deadline`, which will diff the root `Scope`. -To start the diffing process, the component is run. After the root component is run it will look like this: +To start the diffing process, the component function is run. After the root component is run it, the root `Scope` will look like this: [![](https://mermaid.ink/img/pako:eNrFVlFP2zAQ_iuen0BrpCaIl0i8AEJ72KQJtpcRFBnbJVYTn-U4tBXw33dpG5M2CetoBfdkny_ffb67fPIT5SAkjekkhxnPmHXk-3WiCVpZ3T9YZjJyDeDIDQcjycRCQVwmCTOGXEBhQEvtVvG1CWUldwo0-XX-6vVIF5W1GB9cWVbI1_PNL5v8jW3uPFbpmFOc2HK-GfA2WG1ZeJSFx0EQmJxxmUEupE01liEd394mVAkyjolYaFYgfu1P6N1dF8Yzua-cA51WphtTWzsLc872Zan9CnEGUkktuk6fFm_i5NxFRwn9bUimHrIvCT3-N2EBM70j5XBNOTwI5TrxmvQJkr7ELcHx67Jeggz0v92g8q0RaE-iP1193On6NyxecKUeJeFQaSdtTMLu_Xah5ctT_u94Nty2ZwU0zxWfxqQA5PecPq84kq9nfRw7SK0WDiEFZ4O37d34S_-08lFBVfb92KVb5HIrAp0WpjKYKeGyODLz0dohWIkaZNkiJqfkdLvIH6oRaTSoEmm0n06k0a5K0ZdpL61Io0Yt0nfpxc7UQ0_9cJrhyZ8syX-6brS706Mc489Vjja7fbWj3cxDqIdfJJqOaCFtwZTAV8hT7U0ovjBQRmiMS8HsNKGJfsE4Vjm4WWhOY2crOaKVEczJS8WwgAWNJywv0SuFcmB_rJ41y9fNiBqm_wA0MS9_AUuAiy0?type=png)](https://mermaid.live/edit#pako:eNrFVlFP2zAQ_iuen0BrpCaIl0i8AEJ72KQJtpcRFBnbJVYTn-U4tBXw33dpG5M2CetoBfdkny_ffb67fPIT5SAkjekkhxnPmHXk-3WiCVpZ3T9YZjJyDeDIDQcjycRCQVwmCTOGXEBhQEvtVvG1CWUldwo0-XX-6vVIF5W1GB9cWVbI1_PNL5v8jW3uPFbpmFOc2HK-GfA2WG1ZeJSFx0EQmJxxmUEupE01liEd394mVAkyjolYaFYgfu1P6N1dF8Yzua-cA51WphtTWzsLc872Zan9CnEGUkktuk6fFm_i5NxFRwn9bUimHrIvCT3-N2EBM70j5XBNOTwI5TrxmvQJkr7ELcHx67Jeggz0v92g8q0RaE-iP1193On6NyxecKUeJeFQaSdtTMLu_Xah5ctT_u94Nty2ZwU0zxWfxqQA5PecPq84kq9nfRw7SK0WDiEFZ4O37d34S_-08lFBVfb92KVb5HIrAp0WpjKYKeGyODLz0dohWIkaZNkiJqfkdLvIH6oRaTSoEmm0n06k0a5K0ZdpL61Io0Yt0nfpxc7UQ0_9cJrhyZ8syX-6brS706Mc489Vjja7fbWj3cxDqIdfJJqOaCFtwZTAV8hT7U0ovjBQRmiMS8HsNKGJfsE4Vjm4WWhOY2crOaKVEczJS8WwgAWNJywv0SuFcmB_rJ41y9fNiBqm_wA0MS9_AUuAiy0) -Next, the Virtual Dom will compare the new VNode with the previous VNode and only update the parts of the tree that have changed. - -When a component is re-rendered, the Virtual Dom will compare the new VNode with the previous VNode and only update the parts of the tree that have changed. +Next, the Virtual DOM will compare the new VNode with the previous VNode and only update the parts of the tree that have changed. Because of this approach, when a component is re-rendered only the parts of the tree that have changed will be updated in the DOM by the renderer. The diffing algorithm goes through the list of dynamic attributes and nodes and compares them to the previous VNode. If the attribute or node has changed, a mutation that describes the change is added to the mutation list. -Here is what the diffing algorithm looks like for the root scope (red lines indicate that a mutation was generated, and green lines indicate that no mutation was generated) +Here is what the diffing algorithm looks like for the root `Scope` (red lines indicate that a mutation was generated, and green lines indicate that no mutation was generated) [![](https://mermaid.ink/img/pako:eNrFlFFPwjAQx7_KpT7Kko2Elya8qCE-aGLAJ5khpe1Yw9Zbug4k4He3OJjbGPig0T5t17tf_nf777aEo5CEkijBNY-ZsfAwDjW4kxfzhWFZDGNECxOOmYTIYAo2lsCyDG4xzVBLbcv8_RHKSG4V6orSIN0Wxrh8b2RYKr_uTyubd1W92GiWKg7aac6bOU3G803HbVk82xfP_Ok0JEqAT-FeLWJvpFYSOBbaSkMhCMnra5MgtfhWFrPWqHlhL2urT6atbU-oa0PNE8WXFFJ0-nazXakRroddGk9IwYEUnCd5w7Pddr5UTT8ZuVJY5F0fM7ebRLYyXNDgUnprJWxM-9lb7xAQLHe-M2xDYQCD9pD_2hez_kVn-P_rjLq6n3qjYv2iO5qz9DyvPdyv1ETp5eTTJ_7BGvQq8v1TVtl5jXUcRRcrqFh-dI4VtFlBN6t_ynLNkh5JpUmZEm5rbvfhkLiN6H4BQt2jYGYZklC_uzxWWJxsNCfUmkL2SJEJZuWdYs4cKaERS3IXlUJZNI_lGv7cxj2SMf2CeMx5_wBcbK19?type=png)](https://mermaid.live/edit#pako:eNrFlFFPwjAQx7_KpT7Kko2Elya8qCE-aGLAJ5khpe1Yw9Zbug4k4He3OJjbGPig0T5t17tf_nf777aEo5CEkijBNY-ZsfAwDjW4kxfzhWFZDGNECxOOmYTIYAo2lsCyDG4xzVBLbcv8_RHKSG4V6orSIN0Wxrh8b2RYKr_uTyubd1W92GiWKg7aac6bOU3G803HbVk82xfP_Ok0JEqAT-FeLWJvpFYSOBbaSkMhCMnra5MgtfhWFrPWqHlhL2urT6atbU-oa0PNE8WXFFJ0-nazXakRroddGk9IwYEUnCd5w7Pddr5UTT8ZuVJY5F0fM7ebRLYyXNDgUnprJWxM-9lb7xAQLHe-M2xDYQCD9pD_2hez_kVn-P_rjLq6n3qjYv2iO5qz9DyvPdyv1ETp5eTTJ_7BGvQq8v1TVtl5jXUcRRcrqFh-dI4VtFlBN6t_ynLNkh5JpUmZEm5rbvfhkLiN6H4BQt2jYGYZklC_uzxWWJxsNCfUmkL2SJEJZuWdYs4cKaERS3IXlUJZNI_lGv7cxj2SMf2CeMx5_wBcbK19) ## Conclusion -This is only a brief overview of how the Virtual Dom works. There are several aspects not yet covered in this guide including how the Virtual Dom handles async-components, keyed diffing, and how it uses [bump allocation](https://github.com/fitzgen/bumpalo) to efficiently allocate VNodes. If need more information about the Virtual Dom, you can read the code of the [core](https://github.com/DioxusLabs/dioxus/tree/master/packages/core) crate or reach out to us on [Discord](https://discord.gg/XgGxMSkvUM). +This is only a brief overview of how the Virtual Dom works. There are several aspects not yet covered in this guide including: + + * How the Virtual DOM handles async-components + * Keyed diffing + * Using [bump allocation](https://github.com/fitzgen/bumpalo) to efficiently allocate VNodes. + +If you need more information about the Virtual Dom, you can read the code of the [core](https://github.com/DioxusLabs/dioxus/tree/master/packages/core) crate or reach out to us on [Discord](https://discord.gg/XgGxMSkvUM). \ No newline at end of file diff --git a/docs/guide/src/en/fullstack/getting_started.md b/docs/guide/src/en/fullstack/getting_started.md index 9511b8a6c..401af1d43 100644 --- a/docs/guide/src/en/fullstack/getting_started.md +++ b/docs/guide/src/en/fullstack/getting_started.md @@ -76,7 +76,7 @@ Next, we need to modify our `main.rs` to use either hydrate on the client or ren {{#include ../../../examples/hydration.rs}} ``` -Now, build your client-side bundle with `dioxus build --features web` and run your server with `cargo run --features ssr`. You should see the same page as before, but now you can interact with the buttons! +Now, build your client-side bundle with `dx build --features web` and run your server with `cargo run --features ssr`. You should see the same page as before, but now you can interact with the buttons! ## Sycronizing props between the server and client @@ -99,4 +99,4 @@ The only thing we need to change on the client is the props. `dioxus-fullstack` {{#include ../../../examples/hydration_props.rs}} ``` -Now, build your client-side bundle with `dioxus build --features web` and run your server with `cargo run --features ssr`. Navigate to `http://localhost:8080/1` and you should see the counter start at 1. Navigate to `http://localhost:8080/2` and you should see the counter start at 2. +Now, build your client-side bundle with `dx build --features web` and run your server with `cargo run --features ssr`. Navigate to `http://localhost:8080/1` and you should see the counter start at 1. Navigate to `http://localhost:8080/2` and you should see the counter start at 2. diff --git a/docs/guide/src/en/fullstack/server_functions.md b/docs/guide/src/en/fullstack/server_functions.md index 57883885a..581aa382a 100644 --- a/docs/guide/src/en/fullstack/server_functions.md +++ b/docs/guide/src/en/fullstack/server_functions.md @@ -24,7 +24,7 @@ Next, add the server function to your `main.rs`: {{#include ../../../examples/server_function.rs}} ``` -Now, build your client-side bundle with `dioxus build --features web` and run your server with `cargo run --features ssr`. You should see a new button that multiplies the count by 2. +Now, build your client-side bundle with `dx build --features web` and run your server with `cargo run --features ssr`. You should see a new button that multiplies the count by 2. ## Conclusion diff --git a/docs/guide/src/en/getting_started/hot_reload.md b/docs/guide/src/en/getting_started/hot_reload.md index b14de30a4..d207f361a 100644 --- a/docs/guide/src/en/getting_started/hot_reload.md +++ b/docs/guide/src/en/getting_started/hot_reload.md @@ -18,7 +18,7 @@ Hot reloading is automatically enabled when using the web renderer on debug buil 1. Run: ```bash -dioxus serve --hot-reload +dx serve --hot-reload ``` 2. Change some code within a rsx or render macro diff --git a/docs/guide/src/en/getting_started/web.md b/docs/guide/src/en/getting_started/web.md index 7f3d04848..48818bf8f 100644 --- a/docs/guide/src/en/getting_started/web.md +++ b/docs/guide/src/en/getting_started/web.md @@ -59,5 +59,5 @@ Edit your `main.rs`: And to serve our app: ```bash -dioxus serve +dx serve ``` diff --git a/docs/guide/src/en/interactivity/hooks.md b/docs/guide/src/en/interactivity/hooks.md index 6851aacbf..8b571dacd 100644 --- a/docs/guide/src/en/interactivity/hooks.md +++ b/docs/guide/src/en/interactivity/hooks.md @@ -2,15 +2,16 @@ So far our components have had no state like a normal rust functions. However, in a UI component, it is often useful to have stateful functionality to build user interactions. For example, you might want to track whether the user has opened a drop-down, and render different things accordingly. -Hooks allow us to create state in our components. Hooks are Rust functions that take a reference to `ScopeState` (in a component, you can pass `cx`), and provide you with functionality and state. +Hooks allow us to create state in our components. Hooks are Rust functions that take a reference to [`ScopeState`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.ScopeState.html) (in a component, you can pass `cx`), and provide you with functionality and state. ## `use_state` Hook [`use_state`](https://docs.rs/dioxus/latest/dioxus/prelude/fn.use_state.html) is one of the simplest hooks. -- You provide a closure that determines the initial value +- You provide a closure that determines the initial value: `let mut count = use_state(cx, || 0);` - `use_state` gives you the current value, and a way to update it by setting it to something else -- When the value updates, `use_state` makes the component re-render, and provides you with the new value +- When the value updates, `use_state` makes the component re-render (along with any other component + that references it), and then provides you with the new value. For example, you might have seen the counter example, in which state (a number) is tracked using the `use_state` hook: @@ -45,10 +46,11 @@ But how can Dioxus differentiate between multiple hooks in the same component? A This is only possible because the two hooks are always called in the same order, so Dioxus knows which is which. Because the order you call hooks matters, you must follow certain rules when using hooks: 1. Hooks may be only used in components or other hooks (we'll get to that later) -2. On every call to the component function +2. On every call to a component function 1. The same hooks must be called (except in the case of early returns, as explained later in the [Error Handling chapter](../best_practices/error_handling.md)) 2. In the same order -3. Hooks name's should start with `use_` so you don't accidentally confuse them with regular functions +3. Hook names should start with `use_` so you don't accidentally confuse them with regular + functions (`use_state()`, `use_ref()`, `use_future()`, etc...) These rules mean that there are certain things you can't do with hooks: @@ -74,9 +76,12 @@ These rules mean that there are certain things you can't do with hooks: `use_state` is great for tracking simple values. However, you may notice in the [`UseState` API](https://docs.rs/dioxus/latest/dioxus/hooks/struct.UseState.html) that the only way to modify its value is to replace it with something else (e.g., by calling `set`, or through one of the `+=`, `-=` operators). This works well when it is cheap to construct a value (such as any primitive). But what if you want to maintain more complex data in the components state? -For example, suppose we want to maintain a `Vec` of values. If we stored it with `use_state`, the only way to add a new value to the list would be to create a new `Vec` with the additional value, and put it in the state. This is expensive! We want to modify the existing `Vec` instead. +For example, suppose we want to maintain a `Vec` of values. If we stored it with `use_state`, the +only way to add a new value to the list would be to copy the existing `Vec`, add our value to it, +and then replace the existing `Vec` in the state with it. This is expensive! We want to modify the +existing `Vec` instead. -Thankfully, there is another hook for that, `use_ref`! It is similar to `use_state`, but it lets you get a mutable reference to the contained data. +Thankfully, there is another hook for that, `use_ref`! It **is** similar to `use_state`, but it lets you get a mutable reference to the contained data. Here's a simple example that keeps a list of events in a `use_ref`. We can acquire write access to the state with `.with_mut()`, and then just `.push` a new value to the state: @@ -84,4 +89,18 @@ Here's a simple example that keeps a list of events in a `use_ref`. We can acqui {{#include ../../../examples/hooks_use_ref.rs:component}} ``` -> The return values of `use_state` and `use_ref` (`UseState` and `UseRef`, respectively) are in some ways similar to [`Cell`](https://doc.rust-lang.org/std/cell/) and [`RefCell`](https://doc.rust-lang.org/std/cell/struct.RefCell.html) – they provide interior mutability. However, these Dioxus wrappers also ensure that the component gets re-rendered whenever you change the state. +> The return values of `use_state` and `use_ref` ( +> [`UseState`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.UseState.html) and +> [`UseRef`](https://docs.rs/dioxus/latest/dioxus/prelude/struct.UseRef.html), respectively) are in +> some ways similar to [`Cell`](https://doc.rust-lang.org/std/cell/) and +> [`RefCell`](https://doc.rust-lang.org/std/cell/struct.RefCell.html) – they provide interior +> mutability. However, these Dioxus wrappers also ensure that the component gets re-rendered +> whenever you change the state. + + +## Additional Resources + +- [**dioxus_hooks** ](https://docs.rs/dioxus-hooks/latest/dioxus_hooks/) rustdoc + - Documents all hook types included with dioxus by default Most of these are also covered in + later chapters of this guide. +- [Hooks Package](https://github.com/DioxusLabs/dioxus/tree/master/packages/hooks) diff --git a/docs/guide/src/pt-br/getting_started/hot_reload.md b/docs/guide/src/pt-br/getting_started/hot_reload.md index 2dabde536..447169055 100644 --- a/docs/guide/src/pt-br/getting_started/hot_reload.md +++ b/docs/guide/src/pt-br/getting_started/hot_reload.md @@ -18,7 +18,7 @@ dioxus = { version = "*", features = ["hot-reload"] } 1. Execute: ``` -dioxus serve --hot-reload +dx serve --hot-reload ``` 2. alterar algum código dentro de uma macro `rsx` diff --git a/docs/router/examples/catch_all.rs b/docs/router/examples/catch_all.rs index d55b8efcb..38dfa11f1 100644 --- a/docs/router/examples/catch_all.rs +++ b/docs/router/examples/catch_all.rs @@ -8,7 +8,7 @@ enum Route { #[route("/")] Home {}, // PageNotFound is a catch all route that will match any route and placing the matched segments in the route field - #[route("/:...route")] + #[route("/:..route")] PageNotFound { route: Vec }, } // ANCHOR_END: router diff --git a/docs/router/examples/catch_all_segments.rs b/docs/router/examples/catch_all_segments.rs index d4fcb5261..ce84a84db 100644 --- a/docs/router/examples/catch_all_segments.rs +++ b/docs/router/examples/catch_all_segments.rs @@ -7,7 +7,7 @@ use dioxus_router::prelude::*; #[rustfmt::skip] enum Route { // segments that start with :... are catch all segments - #[route("/blog/:...segments")] + #[route("/blog/:..segments")] BlogPost { // You must include catch all segment in child variants segments: Vec, diff --git a/docs/router/examples/dynamic_route.rs b/docs/router/examples/dynamic_route.rs index bb02e57b8..4ea5b3b45 100644 --- a/docs/router/examples/dynamic_route.rs +++ b/docs/router/examples/dynamic_route.rs @@ -26,7 +26,7 @@ enum Route { #[end_layout] #[end_nest] #[end_layout] - #[route("/:...route")] + #[route("/:..route")] PageNotFound { route: Vec, }, diff --git a/docs/router/examples/full_example.rs b/docs/router/examples/full_example.rs index 2f6251968..ff8302926 100644 --- a/docs/router/examples/full_example.rs +++ b/docs/router/examples/full_example.rs @@ -30,7 +30,7 @@ enum Route { #[redirect("/", || Route::BlogList {})] #[redirect("/:name", |name: String| Route::BlogPost { name })] #[end_nest] - #[route("/:...route")] + #[route("/:..route")] PageNotFound { route: Vec, }, diff --git a/docs/router/examples/links.rs b/docs/router/examples/links.rs index a84d50158..c74865927 100644 --- a/docs/router/examples/links.rs +++ b/docs/router/examples/links.rs @@ -11,7 +11,7 @@ enum Route { #[route("/")] Home {}, #[end_layout] - #[route("/:...route")] + #[route("/:..route")] PageNotFound { route: Vec }, } // ANCHOR_END: router diff --git a/docs/router/examples/navigator.rs b/docs/router/examples/navigator.rs index 786f832a4..4c6249366 100644 --- a/docs/router/examples/navigator.rs +++ b/docs/router/examples/navigator.rs @@ -7,7 +7,7 @@ use dioxus_router::prelude::*; enum Route { #[route("/")] Home {}, - #[route("/:...route")] + #[route("/:..route")] PageNotFound { route: Vec }, } diff --git a/docs/router/examples/nested_routes.rs b/docs/router/examples/nested_routes.rs index fa8c81494..7462d8e74 100644 --- a/docs/router/examples/nested_routes.rs +++ b/docs/router/examples/nested_routes.rs @@ -11,7 +11,7 @@ enum Route { #[route("/")] Home {}, #[end_layout] - #[route("/:...route")] + #[route("/:..route")] PageNotFound { route: Vec }, } // ANCHOR_END: router diff --git a/examples/PWA-example/README.md b/examples/PWA-example/README.md index dfd986302..d42a61311 100644 --- a/examples/PWA-example/README.md +++ b/examples/PWA-example/README.md @@ -9,8 +9,8 @@ It is also very much usable as a template for your projects, if you're aiming to Make sure you have Dioxus CLI installed (if you're unsure, run `cargo install dioxus-cli`). -You can run `dioxus serve` in this directory to start the web server locally, or run -`dioxus build --release` to build the project so you can deploy it on a separate web-server. +You can run `dx serve` in this directory to start the web server locally, or run +`dx build --release` to build the project so you can deploy it on a separate web-server. ## Project Structure ``` @@ -41,4 +41,4 @@ For service worker scripting (in JavaScript): * [Service worker guide from PWABuilder](https://docs.pwabuilder.com/#/home/sw-intro) * [Service worker examples, also from PWABuilder](https://github.com/pwa-builder/pwabuilder-serviceworkers) -If you want to stay as close to 100% Rust as possible, you can try using [wasi-worker](https://github.com/dunnock/wasi-worker) to replace the JS service worker file. The JSON manifest will still be required though. \ No newline at end of file +If you want to stay as close to 100% Rust as possible, you can try using [wasi-worker](https://github.com/dunnock/wasi-worker) to replace the JS service worker file. The JSON manifest will still be required though. diff --git a/examples/file_upload.rs b/examples/file_upload.rs index b23648b50..c7358c685 100644 --- a/examples/file_upload.rs +++ b/examples/file_upload.rs @@ -1,32 +1,47 @@ #![allow(non_snake_case)] use dioxus::prelude::*; +use tokio::time::sleep; fn main() { dioxus_desktop::launch(App); } fn App(cx: Scope) -> Element { + let enable_directory_upload = use_state(cx, || false); let files_uploaded: &UseRef> = use_ref(cx, Vec::new); cx.render(rsx! { + label { + input { + r#type: "checkbox", + checked: "{enable_directory_upload}", + oninput: move |evt| { + enable_directory_upload.set(evt.value.parse().unwrap()); + }, + }, + "Enable directory upload" + } + input { r#type: "file", - accept: ".txt, .rs", + accept: ".txt,.rs", multiple: true, + directory: **enable_directory_upload, onchange: |evt| { to_owned![files_uploaded]; async move { if let Some(file_engine) = &evt.files { let files = file_engine.files(); - for file_name in &files { - if let Some(file) = file_engine.read_file_to_string(file_name).await{ - files_uploaded.write().push(file); - } + for file_name in files { + sleep(std::time::Duration::from_secs(1)).await; + files_uploaded.write().push(file_name); } } } }, - } + }, + + div { "progress: {files_uploaded.read().len()}" }, ul { for file in files_uploaded.read().iter() { diff --git a/examples/router.rs b/examples/router.rs index 2f6251968..ff8302926 100644 --- a/examples/router.rs +++ b/examples/router.rs @@ -30,7 +30,7 @@ enum Route { #[redirect("/", || Route::BlogList {})] #[redirect("/:name", |name: String| Route::BlogPost { name })] #[end_nest] - #[route("/:...route")] + #[route("/:..route")] PageNotFound { route: Vec, }, diff --git a/examples/tailwind/README.md b/examples/tailwind/README.md index 0498ecbd3..f2a08eba9 100644 --- a/examples/tailwind/README.md +++ b/examples/tailwind/README.md @@ -122,7 +122,7 @@ npx tailwindcss -i ./input.css -o ./public/tailwind.css --watch - Run the following command in the root of the project to start the dioxus dev server: ```bash -dioxus serve --hot-reload +dx serve --hot-reload ``` - Open the browser to http://localhost:8080 diff --git a/notes/FAQ.md b/notes/FAQ.md index 4e9ea00e4..a94071130 100644 --- a/notes/FAQ.md +++ b/notes/FAQ.md @@ -18,7 +18,7 @@ There are plenty Rust Elm-like frameworks in the world - we were not interested The `RSX` DSL is _barely_ a DSL. Rustaceans will find the DSL very similar to simply assembling nested structs, but without the syntactical overhead of "Default" everywhere or having to jump through hoops with the builder pattern. Between RSX, HTML, the Raw Factory API, and the NodeBuilder syntax, there's plenty of options to choose from. ### What are the build times like? Why on earth would I choose Rust instead of JS/TS/Elm? -Dioxus builds as roughly as fast as a complex WebPack-TypeScript site. Compile times will be slower than an equivalent TypeScript site, but not unbearably slow. The Wasm compiler backend for Rust is very fast. Iterating on small components is basically instant and larger apps takes a few seconds. In practice, the compiler guarantees of Rust balance out the rebuild times. +dx builds as roughly as fast as a complex WebPack-TypeScript site. Compile times will be slower than an equivalent TypeScript site, but not unbearably slow. The Wasm compiler backend for Rust is very fast. Iterating on small components is basically instant and larger apps takes a few seconds. In practice, the compiler guarantees of Rust balance out the rebuild times. ### What about Yew/Seed/Sycamore/Dominator/Dodrio/Percy? - Yew and Seed use an Elm-like pattern and don't support SSR or any alternate rendering platforms diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index c947baa63..3aa741615 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -86,13 +86,6 @@ dioxus-core = { workspace = true, features = ["serialize"] } default = [] plugin = ["mlua"] -# install path dx and dioxus as the same command -# so, they're not really aliases -# eventually dx will defer to the right version of dioxus -[[bin]] -path = "src/main.rs" -name = "dioxus" - [[bin]] path = "src/main.rs" name = "dx" diff --git a/packages/cli/Dioxus.toml b/packages/cli/Dioxus.toml index c3e0dd8a0..792a15165 100644 --- a/packages/cli/Dioxus.toml +++ b/packages/cli/Dioxus.toml @@ -4,7 +4,7 @@ name = "dioxus-cli" # default platfrom -# you can also use `dioxus serve/build --platform XXX` to use other platform +# you can also use `dx serve/build --platform XXX` to use other platform # value: web | desktop default_platform = "desktop" @@ -42,4 +42,4 @@ script = [] # use binaryen.wasm-opt for output Wasm file # binaryen just will trigger in `web` platform -binaryen = { wasm_opt = true } \ No newline at end of file +binaryen = { wasm_opt = true } diff --git a/packages/cli/README.md b/packages/cli/README.md index 6f0826e10..3167a6746 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -24,7 +24,7 @@ cargo install --path . --debug ## Get Started -Use `dioxus create project-name` to initialize a new Dioxus project.
+Use `dx create project-name` to initialize a new Dioxus project.
It will be cloned from the [dioxus-template](https://github.com/DioxusLabs/dioxus-template) repository. @@ -33,7 +33,7 @@ It will be cloned from the [dioxus-template](https://github.com/DioxusLabs/dioxu Alternatively, you can specify the template path: ``` -dioxus create hello --template gh:dioxuslabs/dioxus-template +dx create hello --template gh:dioxuslabs/dioxus-template ``` ## Dioxus Config File diff --git a/packages/cli/docs/src/cmd/README.md b/packages/cli/docs/src/cmd/README.md index dae226577..2de7cc138 100644 --- a/packages/cli/docs/src/cmd/README.md +++ b/packages/cli/docs/src/cmd/README.md @@ -2,14 +2,14 @@ In this chapter we will introduce all `dioxus-cli` commands. -> You can also use `dioxus --help` to get cli help info. +> You can also use `dx --help` to get cli help info. ``` -dioxus +dx Build, bundle, & ship your Dioxus app USAGE: - dioxus [OPTIONS] + dx [OPTIONS] OPTIONS: -h, --help Print help information @@ -23,4 +23,4 @@ SUBCOMMANDS: help Print this message or the help of the given subcommand(s) serve Build, watch & serve the Rust WASM app and all of its assets translate Translate some html file into a Dioxus component -``` \ No newline at end of file +``` diff --git a/packages/cli/docs/src/cmd/build.md b/packages/cli/docs/src/cmd/build.md index 5fde77ef2..6109d14f1 100644 --- a/packages/cli/docs/src/cmd/build.md +++ b/packages/cli/docs/src/cmd/build.md @@ -1,13 +1,13 @@ # Build -The `dioxus build` command can help you `pack & build` a dioxus project. +The `dx build` command can help you `pack & build` a dioxus project. ``` -dioxus-build +dioxus-build Build the Rust WASM app and all of its assets USAGE: - dioxus build [OPTIONS] + dx build [OPTIONS] OPTIONS: --example [default: ""] @@ -19,7 +19,7 @@ OPTIONS: You can use this command to build a project: ``` -dioxus build --release +dx build --release ``` ## Target platform @@ -28,14 +28,14 @@ Use the `platform` option to choose your target platform: ``` # for desktop project -dioxus build --platform desktop +dx build --platform desktop ``` `platform` currently only supports `desktop` & `web`. ``` # for web project -dioxus build --platform web +dx build --platform web ``` ## Specify workspace bin @@ -43,7 +43,7 @@ dioxus build --platform web You can add the `--bin` option to select which crate you want Dioxus to build: ``` -dioxus build --bin app +dx build --bin app ``` ## Build Example @@ -52,5 +52,5 @@ You can use the `example` option to select a example to build: ``` # build the `test` example -dioxus build --exmaple test -``` \ No newline at end of file +dx build --exmaple test +``` diff --git a/packages/cli/docs/src/cmd/clean.md b/packages/cli/docs/src/cmd/clean.md index 72f2342a6..ef938d245 100644 --- a/packages/cli/docs/src/cmd/clean.md +++ b/packages/cli/docs/src/cmd/clean.md @@ -1,13 +1,13 @@ # Clean -`dioxus clean` will clear the build artifacts (the out_dir and the cargo cache) +`dx clean` will clear the build artifacts (the out_dir and the cargo cache) ``` -dioxus-clean +dioxus-clean Clean build artifacts USAGE: - dioxus clean [OPTIONS] + dx clean [OPTIONS] OPTIONS: --bin [default: None] @@ -16,12 +16,12 @@ OPTIONS: # Example ``` -dioxus clean +dx clean ``` # Specify workspace bin You can add the `--bin` option to select which crate you want Dioxus to clean artifacts from: ``` -dioxus clean --bin app -``` \ No newline at end of file +dx clean --bin app +``` diff --git a/packages/cli/docs/src/cmd/serve.md b/packages/cli/docs/src/cmd/serve.md index 695f153bb..d8b4e0d42 100644 --- a/packages/cli/docs/src/cmd/serve.md +++ b/packages/cli/docs/src/cmd/serve.md @@ -1,13 +1,13 @@ # Serve -The `dioxus serve` can start a dev server with hot-reloading +The `dx serve` can start a dev server with hot-reloading ``` -dioxus-serve +dioxus-serve Build, watch & serve the Rust WASM app and all of its assets USAGE: - dioxus serve [OPTIONS] + dx serve [OPTIONS] OPTIONS: --example [default: ""] @@ -20,7 +20,7 @@ OPTIONS: You can use this command to build project and start a dev server: ``` -dioxus serve +dx serve ``` ## Serve Example @@ -29,7 +29,7 @@ You can use the `example` option to serve a example: ``` # serve the `test` example -dioxus serve --exmaple test +dx serve --exmaple test ``` ## Specify workspace bin @@ -37,7 +37,7 @@ dioxus serve --exmaple test You can add the `--bin` option to select which crate you want Dioxus to build and serve: ``` -dioxus serve --bin app +dx serve --bin app ``` ## Open Browser @@ -45,7 +45,7 @@ dioxus serve --bin app You can add the `--open` option to open system default browser when server startup: ``` -dioxus serve --open +dx serve --open ``` ## RSX Hot Reloading @@ -53,7 +53,7 @@ dioxus serve --open You can add the `--hot-reload` flag to enable [rsx hot reloading](https://dioxuslabs.com/docs/0.3/guide/en/getting_started/hot_reload.html). This will allow you to reload some rsx changes without a full recompile: ``` -dioxus serve --open +dx serve --open ``` ## Cross Origin Policy @@ -66,5 +66,5 @@ You can add the `cross-origin-policy` option to change cross-origin header to: ``` ``` -dioxus serve --corss-origin-policy -``` \ No newline at end of file +dx serve --corss-origin-policy +``` diff --git a/packages/cli/docs/src/cmd/translate.md b/packages/cli/docs/src/cmd/translate.md index 7237e1dcf..b03b4422c 100644 --- a/packages/cli/docs/src/cmd/translate.md +++ b/packages/cli/docs/src/cmd/translate.md @@ -1,13 +1,13 @@ # Translate -`dioxus translate` can translate some `html` file into a Dioxus compoent +`dx translate` can translate some `html` file into a Dioxus compoent ``` -dioxus-translate +dioxus-translate Translate some source file into a Dioxus component USAGE: - dioxus translate [OPTIONS] [OUTPUT] + dx translate [OPTIONS] [OUTPUT] ARGS: Output file, defaults to stdout if not present @@ -22,7 +22,7 @@ OPTIONS: You can use the `file` option to set path to the `html` file to translate: ``` -dioxus transtale --file ./index.html +dx transtale --file ./index.html ``` ## Output rsx to a file @@ -30,7 +30,7 @@ dioxus transtale --file ./index.html You can pass a file to the traslate command to set the path to write the output of the command to: ``` -dioxus translate --file ./index.html component.rsx +dx translate --file ./index.html component.rsx ``` ## Output rsx to a file @@ -38,7 +38,7 @@ dioxus translate --file ./index.html component.rsx Setting the `component` option will create a compoent from the HTML: ``` -dioxus translate --file ./index.html --component +dx translate --file ./index.html --component ``` ## Example @@ -65,4 +65,4 @@ fn component(cx: Scope) -> Element { } }) } -``` \ No newline at end of file +``` diff --git a/packages/cli/docs/src/configure.md b/packages/cli/docs/src/configure.md index 35904caae..8da66d9ec 100644 --- a/packages/cli/docs/src/configure.md +++ b/packages/cli/docs/src/configure.md @@ -29,8 +29,8 @@ General application confiration: # default: web default_platform = "web" ``` - if you change this to `desktop`, the `dioxus build` will default building a desktop app -3. ***out_dir*** - The directory to place the build artifacts from `dioxus build` or `dioxus service` into. This is also where the `assets` directory will be copied to + if you change this to `desktop`, the `dx build` will default building a desktop app +3. ***out_dir*** - The directory to place the build artifacts from `dx build` or `dx service` into. This is also where the `assets` directory will be copied to ``` out_dir = "dist" ``` diff --git a/packages/cli/docs/src/creating.md b/packages/cli/docs/src/creating.md index 5bb98fd10..6c2d5754c 100644 --- a/packages/cli/docs/src/creating.md +++ b/packages/cli/docs/src/creating.md @@ -4,10 +4,10 @@ Once you have the Dioxus CLI tool installed, you can use it to create dioxus pro ## Initializing a default project -First, run the `dioxus create` command to create a new project ready to be used with Dioxus and the Dioxus CLI: +First, run the `dx create` command to create a new project ready to be used with Dioxus and the Dioxus CLI: ``` -dioxus create hello-dioxus +dx create hello-dioxus ``` > It will clone a default template from github template: [DioxusLabs/dioxus-template](https://github.com/DioxusLabs/dioxus-template) @@ -15,7 +15,7 @@ dioxus create hello-dioxus > > You can choose to create your project from a different template by passing the `template` argument: > ``` -> dioxus init hello-dioxus --template=gh:dioxuslabs/dioxus-template +> dx init hello-dioxus --template=gh:dioxuslabs/dioxus-template > ``` Next, move the current directory into your new project: @@ -33,7 +33,7 @@ cd hello-dioxus Finally, create serve your project with the Dioxus CLI: ``` -dioxus serve +dx serve ``` By default, the CLI serve your site at: [`http://127.0.0.1:8080/`](http://127.0.0.1:8080/) diff --git a/packages/cli/docs/src/plugin/README.md b/packages/cli/docs/src/plugin/README.md index 9983f024b..0a1a7b606 100644 --- a/packages/cli/docs/src/plugin/README.md +++ b/packages/cli/docs/src/plugin/README.md @@ -2,7 +2,7 @@ > For Cli 0.2.0 we will add `plugin-develop` support. -Before the 0.2.0 we use `dioxus tool` to use & install some plugin, but we think that is not good for extend cli program, some people want tailwind support, some people want sass support, we can't add all this thing in to the cli source code and we don't have time to maintain a lot of tools that user request, so maybe user make plugin by themself is a good choice. +Before the 0.2.0 we use `dx tool` to use & install some plugin, but we think that is not good for extend cli program, some people want tailwind support, some people want sass support, we can't add all this thing in to the cli source code and we don't have time to maintain a lot of tools that user request, so maybe user make plugin by themself is a good choice. ### Why Lua ? @@ -76,4 +76,4 @@ end manager.serve.interval = 1000 return manager -``` \ No newline at end of file +``` diff --git a/packages/cli/src/assets/dioxus.toml b/packages/cli/src/assets/dioxus.toml index 3827d0f8f..dfdeb9f92 100644 --- a/packages/cli/src/assets/dioxus.toml +++ b/packages/cli/src/assets/dioxus.toml @@ -4,7 +4,7 @@ name = "{{project-name}}" # default platfrom -# you can also use `dioxus serve/build --platform XXX` to use other platform +# you can also use `dx serve/build --platform XXX` to use other platform # value: web | desktop default_platform = "{{default-platform}}" diff --git a/packages/cli/src/builder.rs b/packages/cli/src/builder.rs index 6f50fc49d..cf7ccd7b3 100644 --- a/packages/cli/src/builder.rs +++ b/packages/cli/src/builder.rs @@ -161,7 +161,7 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result { } } else { log::warn!( - "Binaryen tool not found, you can use `dioxus tool add binaryen` to install it." + "Binaryen tool not found, you can use `dx tool add binaryen` to install it." ); } } @@ -200,7 +200,7 @@ pub fn build(config: &CrateConfig, quiet: bool) -> Result { } } else { log::warn!( - "Tailwind tool not found, you can use `dioxus tool add tailwindcss` to install it." + "Tailwind tool not found, you can use `dx tool add tailwindcss` to install it." ); } } diff --git a/packages/cli/src/cli/autoformat.rs b/packages/cli/src/cli/autoformat.rs index fbc34402b..ea2c27148 100644 --- a/packages/cli/src/cli/autoformat.rs +++ b/packages/cli/src/cli/autoformat.rs @@ -136,12 +136,16 @@ async fn autoformat_project(check: bool) -> Result<()> { } fn collect_rs_files(folder: &Path, files: &mut Vec) { - let Ok(folder) = folder.read_dir() else { return }; + let Ok(folder) = folder.read_dir() else { + return; + }; // load the gitignore for entry in folder { - let Ok(entry) = entry else { continue; }; + let Ok(entry) = entry else { + continue; + }; let path = entry.path(); diff --git a/packages/cli/src/cli/plugin.rs b/packages/cli/src/cli/plugin.rs index 812c3ae1c..9f514edec 100644 --- a/packages/cli/src/cli/plugin.rs +++ b/packages/cli/src/cli/plugin.rs @@ -31,7 +31,7 @@ impl Plugin { } } Plugin::Add { name: _ } => { - log::info!("You can use `dioxus plugin app-path` to get Installation position"); + log::info!("You can use `dx plugin app-path` to get Installation position"); } } Ok(()) diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index 8efdf13e4..6159c882b 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -88,8 +88,7 @@ impl<'b> VirtualDom { } // Intialize the root nodes slice - node.root_ids - .intialize(vec![ElementId(0); node.template.get().roots.len()].into_boxed_slice()); + *node.root_ids.borrow_mut() = vec![ElementId(0); node.template.get().roots.len()]; // The best renderers will have templates prehydrated and registered // Just in case, let's create the template using instructions anyways @@ -328,7 +327,7 @@ impl<'b> VirtualDom { fn load_template_root(&mut self, template: &VNode, root_idx: usize) -> ElementId { // Get an ID for this root since it's a real root let this_id = self.next_root(template, root_idx); - template.root_ids.set(root_idx, this_id); + template.root_ids.borrow_mut()[root_idx] = this_id; self.mutations.push(LoadTemplate { name: template.template.get().name, diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index cfda18505..74988210a 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -129,12 +129,14 @@ impl<'b> VirtualDom { }); // Make sure the roots get transferred over while we're here - right_template.root_ids.transfer(&left_template.root_ids); + *right_template.root_ids.borrow_mut() = left_template.root_ids.borrow().clone(); + + let root_ids = right_template.root_ids.borrow(); // Update the node refs - for i in 0..right_template.root_ids.len() { - if let Some(root_id) = right_template.root_ids.get(i) { - self.update_template(root_id, right_template); + for i in 0..root_ids.len() { + if let Some(root_id) = root_ids.get(i) { + self.update_template(*root_id, right_template); } } } @@ -686,7 +688,7 @@ impl<'b> VirtualDom { Some(node) => node, None => { self.mutations.push(Mutation::PushRoot { - id: node.root_ids.get(idx).unwrap(), + id: node.root_ids.borrow()[idx], }); return 1; } @@ -821,7 +823,7 @@ impl<'b> VirtualDom { if let Some(dy) = node.dynamic_root(idx) { self.remove_dynamic_node(dy, gen_muts); } else { - let id = node.root_ids.get(idx).unwrap(); + let id = node.root_ids.borrow()[idx]; if gen_muts { self.mutations.push(Mutation::Remove { id }); } @@ -928,7 +930,7 @@ impl<'b> VirtualDom { fn find_first_element(&self, node: &'b VNode<'b>) -> ElementId { match node.dynamic_root(0) { - None => node.root_ids.get(0).unwrap(), + None => node.root_ids.borrow()[0], Some(Text(t)) => t.id.get().unwrap(), Some(Fragment(t)) => self.find_first_element(&t[0]), Some(Placeholder(t)) => t.id.get().unwrap(), @@ -944,7 +946,7 @@ impl<'b> VirtualDom { fn find_last_element(&self, node: &'b VNode<'b>) -> ElementId { match node.dynamic_root(node.template.get().roots.len() - 1) { - None => node.root_ids.last().unwrap(), + None => *node.root_ids.borrow().last().unwrap(), Some(Text(t)) => t.id.get().unwrap(), Some(Fragment(t)) => self.find_last_element(t.last().unwrap()), Some(Placeholder(t)) => t.id.get().unwrap(), diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 209c37c77..197550a3b 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -5,7 +5,7 @@ use bumpalo::boxed::Box as BumpBox; use bumpalo::Bump; use std::{ any::{Any, TypeId}, - cell::{Cell, RefCell, UnsafeCell}, + cell::{Cell, RefCell}, fmt::{Arguments, Debug}, }; @@ -54,7 +54,7 @@ pub struct VNode<'a> { /// The IDs for the roots of this template - to be used when moving the template around and removing it from /// the actual Dom - pub root_ids: BoxedCellSlice, + pub root_ids: RefCell>, /// The dynamic parts of the template pub dynamic_nodes: &'a [DynamicNode<'a>], @@ -63,112 +63,13 @@ pub struct VNode<'a> { pub dynamic_attrs: &'a [Attribute<'a>], } -// Saftey: There is no way to get references to the internal data of this struct so no refrences will be invalidated by mutating the data with a immutable reference (The same principle behind Cell) -#[derive(Debug, Default)] -pub struct BoxedCellSlice(UnsafeCell>>); - -impl Clone for BoxedCellSlice { - fn clone(&self) -> Self { - Self(UnsafeCell::new(unsafe { (*self.0.get()).clone() })) - } -} - -impl BoxedCellSlice { - pub fn last(&self) -> Option { - unsafe { - (*self.0.get()) - .as_ref() - .and_then(|inner| inner.as_ref().last().copied()) - } - } - - pub fn get(&self, idx: usize) -> Option { - unsafe { - (*self.0.get()) - .as_ref() - .and_then(|inner| inner.as_ref().get(idx).copied()) - } - } - - pub unsafe fn get_unchecked(&self, idx: usize) -> Option { - (*self.0.get()) - .as_ref() - .and_then(|inner| inner.as_ref().get(idx).copied()) - } - - pub fn set(&self, idx: usize, new: ElementId) { - unsafe { - if let Some(inner) = &mut *self.0.get() { - inner[idx] = new; - } - } - } - - pub fn intialize(&self, contents: Box<[ElementId]>) { - unsafe { - *self.0.get() = Some(contents); - } - } - - pub fn transfer(&self, other: &Self) { - unsafe { - *self.0.get() = (*other.0.get()).clone(); - } - } - - pub fn take_from(&self, other: &Self) { - unsafe { - *self.0.get() = (*other.0.get()).take(); - } - } - - pub fn len(&self) -> usize { - unsafe { - (*self.0.get()) - .as_ref() - .map(|inner| inner.len()) - .unwrap_or(0) - } - } -} - -impl<'a> IntoIterator for &'a BoxedCellSlice { - type Item = ElementId; - - type IntoIter = BoxedCellSliceIter<'a>; - - fn into_iter(self) -> Self::IntoIter { - BoxedCellSliceIter { - index: 0, - borrow: self, - } - } -} - -pub struct BoxedCellSliceIter<'a> { - index: usize, - borrow: &'a BoxedCellSlice, -} - -impl Iterator for BoxedCellSliceIter<'_> { - type Item = ElementId; - - fn next(&mut self) -> Option { - let result = self.borrow.get(self.index); - if result.is_some() { - self.index += 1; - } - result - } -} - impl<'a> VNode<'a> { /// Create a template with no nodes that will be skipped over during diffing pub fn empty() -> Element<'a> { Some(VNode { key: None, parent: None, - root_ids: BoxedCellSlice::default(), + root_ids: Default::default(), dynamic_nodes: &[], dynamic_attrs: &[], template: Cell::new(Template { diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index f1635dd61..e8134c238 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -87,7 +87,7 @@ impl VirtualDom { if matches!(allocated, RenderReturn::Aborted(_)) { self.suspended_scopes.insert(scope.id); } - } else { + } else if !self.suspended_scopes.is_empty() { _ = self.suspended_scopes.remove(&scope.id); } diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 8d980222c..33fb9aac5 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -693,13 +693,13 @@ impl<'src> ScopeState { raw_ref.downcast_mut::() }) .expect( - r###" + r#" Unable to retrieve the hook that was initialized at this index. Consult the `rules of hooks` to understand how to use hooks properly. You likely used the hook in a conditional. Hooks rely on consistent ordering between renders. Functions prefixed with "use" should never be called conditionally. - "###, + "#, ) } } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 31a1b5912..ee6e9c5d6 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -571,12 +571,15 @@ impl VirtualDom { /// The mutations will be thrown out, so it's best to use this method for things like SSR that have async content pub async fn wait_for_suspense(&mut self) { loop { + // println!("waiting for suspense {:?}", self.suspended_scopes); if self.suspended_scopes.is_empty() { return; } + // println!("waiting for suspense"); self.wait_for_work().await; + // println!("Rendered immediately"); _ = self.render_immediate(); } } diff --git a/packages/core/tests/task.rs b/packages/core/tests/task.rs index 2704c5eb2..316cea6dd 100644 --- a/packages/core/tests/task.rs +++ b/packages/core/tests/task.rs @@ -1,9 +1,9 @@ //! Verify that tasks get polled by the virtualdom properly, and that we escape wait_for_work safely use dioxus::prelude::*; -use std::time::Duration; +use std::{sync::atomic::AtomicUsize, time::Duration}; -static mut POLL_COUNT: usize = 0; +static POLL_COUNT: AtomicUsize = AtomicUsize::new(0); #[tokio::test] async fn it_works() { @@ -18,7 +18,10 @@ async fn it_works() { // By the time the tasks are finished, we should've accumulated ticks from two tasks // Be warned that by setting the delay to too short, tokio might not schedule in the tasks - assert_eq!(unsafe { POLL_COUNT }, 135); + assert_eq!( + POLL_COUNT.fetch_add(0, std::sync::atomic::Ordering::Relaxed), + 135 + ); } fn app(cx: Scope) -> Element { @@ -26,14 +29,14 @@ fn app(cx: Scope) -> Element { cx.spawn(async { for x in 0..10 { tokio::time::sleep(Duration::from_micros(50)).await; - unsafe { POLL_COUNT += x } + POLL_COUNT.fetch_add(x, std::sync::atomic::Ordering::Relaxed); } }); cx.spawn(async { for x in 0..10 { tokio::time::sleep(Duration::from_micros(25)).await; - unsafe { POLL_COUNT += x * 2 } + POLL_COUNT.fetch_add(x * 2, std::sync::atomic::Ordering::Relaxed); } }); }); diff --git a/packages/desktop/src/file_upload.rs b/packages/desktop/src/file_upload.rs index d09783bcf..778c209ac 100644 --- a/packages/desktop/src/file_upload.rs +++ b/packages/desktop/src/file_upload.rs @@ -8,11 +8,48 @@ pub(crate) struct FileDialogRequest { #[serde(default)] accept: Option, multiple: bool, + directory: bool, pub event: String, pub target: usize, pub bubbles: bool, } +fn get_file_event_for_folder(request: &FileDialogRequest, dialog: rfd::FileDialog) -> Vec { + if request.multiple { + dialog.pick_folders().into_iter().flatten().collect() + } else { + dialog.pick_folder().into_iter().collect() + } +} + +fn get_file_event_for_file( + request: &FileDialogRequest, + mut dialog: rfd::FileDialog, +) -> Vec { + let filters: Vec<_> = request + .accept + .as_deref() + .unwrap_or_default() + .split(',') + .filter_map(|s| Filters::from_str(s).ok()) + .collect(); + + let file_extensions: Vec<_> = filters + .iter() + .flat_map(|f| f.as_extensions().into_iter()) + .collect(); + + dialog = dialog.add_filter("name", file_extensions.as_slice()); + + let files: Vec<_> = if request.multiple { + dialog.pick_files().into_iter().flatten().collect() + } else { + dialog.pick_file().into_iter().collect() + }; + + files +} + #[cfg(not(any( target_os = "windows", target_os = "macos", @@ -36,30 +73,13 @@ pub(crate) fn get_file_event(_request: &FileDialogRequest) -> Vec { target_os = "openbsd" ))] pub(crate) fn get_file_event(request: &FileDialogRequest) -> Vec { - let mut dialog = rfd::FileDialog::new(); + let dialog = rfd::FileDialog::new(); - let filters: Vec<_> = request - .accept - .as_deref() - .unwrap_or_default() - .split(',') - .filter_map(|s| Filters::from_str(s).ok()) - .collect(); - - let file_extensions: Vec<_> = filters - .iter() - .flat_map(|f| f.as_extensions().into_iter()) - .collect(); - - dialog = dialog.add_filter("name", file_extensions.as_slice()); - - let files: Vec<_> = if request.multiple { - dialog.pick_files().into_iter().flatten().collect() + if request.directory { + get_file_event_for_folder(request, dialog) } else { - dialog.pick_file().into_iter().collect() - }; - - files + get_file_event_for_file(request, dialog) + } } enum Filters { diff --git a/packages/desktop/src/protocol.rs b/packages/desktop/src/protocol.rs index 6be5d08b4..89ce50a48 100644 --- a/packages/desktop/src/protocol.rs +++ b/packages/desktop/src/protocol.rs @@ -24,7 +24,7 @@ fn module_loader(root_name: &str) -> String { let target_id = find_real_id(target); if (target_id !== null) { const send = (event_name) => { - const message = serializeIpcMessage("file_diolog", { accept: target.getAttribute("accept"), multiple: target.hasAttribute("multiple"), target: parseInt(target_id), bubbles: event_bubbles(event_name), event: event_name }); + const message = serializeIpcMessage("file_diolog", { accept: target.getAttribute("accept"), directory: target.getAttribute("webkitdirectory") === "true", multiple: target.hasAttribute("multiple"), target: parseInt(target_id), bubbles: event_bubbles(event_name), event: event_name }); window.ipc.postMessage(message); }; send("change&input"); diff --git a/packages/dioxus-tui/examples/colorpicker.rs b/packages/dioxus-tui/examples/colorpicker.rs index 1de6b8209..00f8ef7e0 100644 --- a/packages/dioxus-tui/examples/colorpicker.rs +++ b/packages/dioxus-tui/examples/colorpicker.rs @@ -20,7 +20,7 @@ fn app(cx: Scope) -> Element { background_color: "hsl({hue}, 70%, {brightness}%)", onmousemove: move |evt| { if let RenderReturn::Ready(node) = cx.root_node() { - if let Some(id) = node.root_ids.get(0){ + if let Some(id) = node.root_ids.borrow().get(0).cloned() { let node = tui_query.get(mapping.get_node_id(id).unwrap()); let Size{width, height} = node.size().unwrap(); let pos = evt.inner().element_coordinates(); diff --git a/packages/fullstack/examples/axum-auth/src/main.rs b/packages/fullstack/examples/axum-auth/src/main.rs index 00ddb95f9..fe94c9628 100644 --- a/packages/fullstack/examples/axum-auth/src/main.rs +++ b/packages/fullstack/examples/axum-auth/src/main.rs @@ -1,7 +1,7 @@ //! Run with: //! //! ```sh -//! dioxus build --features web +//! dx build --features web //! cargo run --features ssr //! ``` diff --git a/packages/fullstack/examples/axum-hello-world/src/main.rs b/packages/fullstack/examples/axum-hello-world/src/main.rs index cabe4b00e..8a460c685 100644 --- a/packages/fullstack/examples/axum-hello-world/src/main.rs +++ b/packages/fullstack/examples/axum-hello-world/src/main.rs @@ -1,7 +1,7 @@ //! Run with: //! //! ```sh -//! dioxus build --features web +//! dx build --features web //! cargo run --features ssr //! ``` diff --git a/packages/fullstack/examples/axum-router/src/main.rs b/packages/fullstack/examples/axum-router/src/main.rs index f78904277..54ce11769 100644 --- a/packages/fullstack/examples/axum-router/src/main.rs +++ b/packages/fullstack/examples/axum-router/src/main.rs @@ -1,7 +1,7 @@ //! Run with: //! //! ```sh -//! dioxus build --features web +//! dx build --features web //! cargo run --features ssr //! ``` diff --git a/packages/fullstack/examples/salvo-hello-world/src/main.rs b/packages/fullstack/examples/salvo-hello-world/src/main.rs index 765b1836a..d9dcea86f 100644 --- a/packages/fullstack/examples/salvo-hello-world/src/main.rs +++ b/packages/fullstack/examples/salvo-hello-world/src/main.rs @@ -1,7 +1,7 @@ //! Run with: //! //! ```sh -//! dioxus build --features web +//! dx build --features web //! cargo run --features ssr //! ``` diff --git a/packages/fullstack/examples/static-hydrated/src/main.rs b/packages/fullstack/examples/static-hydrated/src/main.rs index eedc0187e..f89b01cce 100644 --- a/packages/fullstack/examples/static-hydrated/src/main.rs +++ b/packages/fullstack/examples/static-hydrated/src/main.rs @@ -1,7 +1,7 @@ //! Run with: //! //! ```sh -//! dioxus build --features web +//! dx build --features web //! cargo run --features ssr //! ``` diff --git a/packages/fullstack/examples/warp-hello-world/src/main.rs b/packages/fullstack/examples/warp-hello-world/src/main.rs index 765b1836a..d9dcea86f 100644 --- a/packages/fullstack/examples/warp-hello-world/src/main.rs +++ b/packages/fullstack/examples/warp-hello-world/src/main.rs @@ -1,7 +1,7 @@ //! Run with: //! //! ```sh -//! dioxus build --features web +//! dx build --features web //! cargo run --features ssr //! ``` diff --git a/packages/html/src/elements.rs b/packages/html/src/elements.rs index e1e80057e..514aea2d1 100644 --- a/packages/html/src/elements.rs +++ b/packages/html/src/elements.rs @@ -1098,6 +1098,7 @@ builder_constructors! { autofocus: Bool DEFAULT, capture: String DEFAULT, checked: Bool DEFAULT, + directory: Bool "webkitdirectory", disabled: Bool DEFAULT, form: Id DEFAULT, formaction: Uri DEFAULT, diff --git a/packages/html/src/events/form.rs b/packages/html/src/events/form.rs index 113c765c5..1fe59a640 100644 --- a/packages/html/src/events/form.rs +++ b/packages/html/src/events/form.rs @@ -56,10 +56,9 @@ where { use serde::Deserialize; - let Ok(file_engine) = - SerializedFileEngine::deserialize(deserializer) else{ - return Ok(None); - }; + let Ok(file_engine) = SerializedFileEngine::deserialize(deserializer) else { + return Ok(None); + }; let file_engine = std::sync::Arc::new(file_engine); Ok(Some(file_engine)) diff --git a/packages/interpreter/src/common.js b/packages/interpreter/src/common.js index c93f9eb60..1583da10e 100644 --- a/packages/interpreter/src/common.js +++ b/packages/interpreter/src/common.js @@ -25,6 +25,7 @@ const bool_attrs = { reversed: true, selected: true, truespeed: true, + webkitdirectory: true, }; export function setAttributeInner(node, field, value, ns) { diff --git a/packages/interpreter/src/sledgehammer_bindings.rs b/packages/interpreter/src/sledgehammer_bindings.rs index e661a7c16..2eb883500 100644 --- a/packages/interpreter/src/sledgehammer_bindings.rs +++ b/packages/interpreter/src/sledgehammer_bindings.rs @@ -163,6 +163,7 @@ mod js { reversed: true, selected: true, truespeed: true, + webkitdirectory: true, }; function truthy(val) { return val === "true" || val === true; diff --git a/packages/liveview/src/adapters/axum_adapter.rs b/packages/liveview/src/adapters/axum_adapter.rs index 646cde38e..dda37acc1 100644 --- a/packages/liveview/src/adapters/axum_adapter.rs +++ b/packages/liveview/src/adapters/axum_adapter.rs @@ -11,13 +11,14 @@ pub fn axum_socket(ws: WebSocket) -> impl LiveViewSocket { .sink_map_err(|_| LiveViewError::SendingFailed) } -fn transform_rx(message: Result) -> Result { +fn transform_rx(message: Result) -> Result, LiveViewError> { message .map_err(|_| LiveViewError::SendingFailed)? .into_text() + .map(|s| s.into_bytes()) .map_err(|_| LiveViewError::SendingFailed) } -async fn transform_tx(message: String) -> Result { - Ok(Message::Text(message)) +async fn transform_tx(message: Vec) -> Result { + Ok(Message::Text(String::from_utf8_lossy(&message).to_string())) } diff --git a/packages/liveview/src/adapters/salvo_adapter.rs b/packages/liveview/src/adapters/salvo_adapter.rs index 2c8912a57..138196da7 100644 --- a/packages/liveview/src/adapters/salvo_adapter.rs +++ b/packages/liveview/src/adapters/salvo_adapter.rs @@ -12,14 +12,12 @@ pub fn salvo_socket(ws: WebSocket) -> impl LiveViewSocket { .sink_map_err(|_| LiveViewError::SendingFailed) } -fn transform_rx(message: Result) -> Result { +fn transform_rx(message: Result) -> Result, LiveViewError> { let as_bytes = message.map_err(|_| LiveViewError::SendingFailed)?; - let msg = String::from_utf8(as_bytes.into_bytes()).map_err(|_| LiveViewError::SendingFailed)?; - - Ok(msg) + Ok(as_bytes.into()) } -async fn transform_tx(message: String) -> Result { - Ok(Message::text(message)) +async fn transform_tx(message: Vec) -> Result { + Ok(Message::text(String::from_utf8_lossy(&message).to_string())) } diff --git a/packages/liveview/src/adapters/warp_adapter.rs b/packages/liveview/src/adapters/warp_adapter.rs index e5c821ce3..9ee8c6fe4 100644 --- a/packages/liveview/src/adapters/warp_adapter.rs +++ b/packages/liveview/src/adapters/warp_adapter.rs @@ -11,18 +11,15 @@ pub fn warp_socket(ws: WebSocket) -> impl LiveViewSocket { .sink_map_err(|_| LiveViewError::SendingFailed) } -fn transform_rx(message: Result) -> Result { +fn transform_rx(message: Result) -> Result, LiveViewError> { // destructure the message into the buffer we got from warp let msg = message .map_err(|_| LiveViewError::SendingFailed)? .into_bytes(); - // transform it back into a string, saving us the allocation - let msg = String::from_utf8(msg).map_err(|_| LiveViewError::SendingFailed)?; - Ok(msg) } -async fn transform_tx(message: String) -> Result { - Ok(Message::text(message)) +async fn transform_tx(message: Vec) -> Result { + Ok(Message::text(String::from_utf8_lossy(&message).to_string())) } diff --git a/packages/liveview/src/pool.rs b/packages/liveview/src/pool.rs index 089d0bad4..dc7e4ea9e 100644 --- a/packages/liveview/src/pool.rs +++ b/packages/liveview/src/pool.rs @@ -87,16 +87,16 @@ impl LiveViewPool { /// } /// ``` pub trait LiveViewSocket: - SinkExt - + StreamExt> + SinkExt, Error = LiveViewError> + + StreamExt, LiveViewError>> + Send + 'static { } impl LiveViewSocket for S where - S: SinkExt - + StreamExt> + S: SinkExt, Error = LiveViewError> + + StreamExt, LiveViewError>> + Send + 'static { @@ -126,7 +126,7 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li pin_mut!(ws); // send the initial render to the client - ws.send(edits).await?; + ws.send(edits.into_bytes()).await?; // Create the a proxy for query engine let (query_tx, mut query_rx) = tokio::sync::mpsc::unbounded_channel(); @@ -156,11 +156,11 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li evt = ws.next() => { match evt.as_ref().map(|o| o.as_deref()) { // respond with a pong every ping to keep the websocket alive - Some(Ok("__ping__")) => { - ws.send("__pong__".to_string()).await?; + Some(Ok(b"__ping__")) => { + ws.send(b"__pong__".to_vec()).await?; } Some(Ok(evt)) => { - if let Ok(message) = serde_json::from_str::(evt) { + if let Ok(message) = serde_json::from_str::(&String::from_utf8_lossy(evt)) { match message { IpcMessage::Event(evt) => { // Intercept the mounted event and insert a custom element type @@ -196,7 +196,7 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li // handle any new queries Some(query) = query_rx.recv() => { - ws.send(serde_json::to_string(&ClientUpdate::Query(query)).unwrap()).await?; + ws.send(serde_json::to_string(&ClientUpdate::Query(query)).unwrap().into_bytes()).await?; } Some(msg) = hot_reload_wait => { @@ -218,8 +218,12 @@ pub async fn run(mut vdom: VirtualDom, ws: impl LiveViewSocket) -> Result<(), Li .render_with_deadline(tokio::time::sleep(Duration::from_millis(10))) .await; - ws.send(serde_json::to_string(&ClientUpdate::Edits(edits)).unwrap()) - .await?; + ws.send( + serde_json::to_string(&ClientUpdate::Edits(edits)) + .unwrap() + .into_bytes(), + ) + .await?; } } diff --git a/packages/rink/src/widgets/button.rs b/packages/rink/src/widgets/button.rs index e4d3754f1..9018a3e9a 100644 --- a/packages/rink/src/widgets/button.rs +++ b/packages/rink/src/widgets/button.rs @@ -82,7 +82,9 @@ impl Button { fn write_value(&self, rdom: &mut RealDom) { if let Some(mut text) = rdom.get_mut(self.text_id) { let node_type = text.node_type_mut(); - let NodeTypeMut::Text(mut text) = node_type else { panic!("input must be an element") }; + let NodeTypeMut::Text(mut text) = node_type else { + panic!("input must be an element") + }; *text.text_mut() = self.value.clone(); } } @@ -111,7 +113,9 @@ impl CustomElement for Button { fn create(mut root: dioxus_native_core::real_dom::NodeMut) -> Self { let node_type = root.node_type(); - let NodeType::Element(el) = &*node_type else { panic!("input must be an element") }; + let NodeType::Element(el) = &*node_type else { + panic!("input must be an element") + }; let value = el .attributes @@ -146,7 +150,9 @@ impl CustomElement for Button { AttributeMask::All => { { let node_type = root.node_type_mut(); - let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") }; + let NodeTypeMut::Element(mut el) = node_type else { + panic!("input must be an element") + }; self.update_value_attr(&el); self.update_size_attr(&mut el); } @@ -155,7 +161,9 @@ impl CustomElement for Button { AttributeMask::Some(attrs) => { { let node_type = root.node_type_mut(); - let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") }; + let NodeTypeMut::Element(mut el) = node_type else { + panic!("input must be an element") + }; if attrs.contains("width") || attrs.contains("height") { self.update_size_attr(&mut el); } diff --git a/packages/rink/src/widgets/checkbox.rs b/packages/rink/src/widgets/checkbox.rs index 24b3f6244..fab809ed2 100644 --- a/packages/rink/src/widgets/checkbox.rs +++ b/packages/rink/src/widgets/checkbox.rs @@ -94,14 +94,18 @@ impl CheckBox { fn write_value(&self, mut root: NodeMut) { let single_char = { let node_type = root.node_type_mut(); - let NodeTypeMut::Element( el) = node_type else { panic!("input must be an element") }; + let NodeTypeMut::Element(el) = node_type else { + panic!("input must be an element") + }; Self::width(&el) == "1px" || Self::height(&el) == "1px" }; let rdom = root.real_dom_mut(); if let Some(mut text) = rdom.get_mut(self.text_id) { let node_type = text.node_type_mut(); - let NodeTypeMut::Text(mut text) = node_type else { panic!("input must be an element") }; + let NodeTypeMut::Text(mut text) = node_type else { + panic!("input must be an element") + }; let value = if single_char { if self.checked { "☑" @@ -156,7 +160,9 @@ impl CustomElement for CheckBox { fn create(mut root: dioxus_native_core::real_dom::NodeMut) -> Self { let node_type = root.node_type(); - let NodeType::Element(el) = &*node_type else { panic!("input must be an element") }; + let NodeType::Element(el) = &*node_type else { + panic!("input must be an element") + }; let value = el .attributes @@ -197,7 +203,9 @@ impl CustomElement for CheckBox { AttributeMask::All => { { let node_type = root.node_type_mut(); - let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") }; + let NodeTypeMut::Element(mut el) = node_type else { + panic!("input must be an element") + }; self.update_value_attr(&el); self.update_size_attr(&mut el); self.update_checked_attr(&el); @@ -207,7 +215,9 @@ impl CustomElement for CheckBox { AttributeMask::Some(attrs) => { { let node_type = root.node_type_mut(); - let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") }; + let NodeTypeMut::Element(mut el) = node_type else { + panic!("input must be an element") + }; if attrs.contains("width") || attrs.contains("height") { self.update_size_attr(&mut el); } diff --git a/packages/rink/src/widgets/input.rs b/packages/rink/src/widgets/input.rs index c6a3e56b9..c1fb563d7 100644 --- a/packages/rink/src/widgets/input.rs +++ b/packages/rink/src/widgets/input.rs @@ -56,7 +56,9 @@ impl CustomElement for Input { } let node_type = root.node_type(); - let NodeType::Element(el) = &*node_type else { panic!("input must be an element") }; + let NodeType::Element(el) = &*node_type else { + panic!("input must be an element") + }; let input_type = el .attributes .get(&OwnedAttributeDiscription { diff --git a/packages/rink/src/widgets/slider.rs b/packages/rink/src/widgets/slider.rs index f97c12bf6..7080b6c12 100644 --- a/packages/rink/src/widgets/slider.rs +++ b/packages/rink/src/widgets/slider.rs @@ -163,7 +163,9 @@ impl Slider { if let Some(mut div) = rdom.get_mut(self.pre_cursor_div) { let node_type = div.node_type_mut(); - let NodeTypeMut::Element(mut element) = node_type else { panic!("input must be an element") }; + let NodeTypeMut::Element(mut element) = node_type else { + panic!("input must be an element") + }; element.set_attribute( OwnedAttributeDiscription { name: "width".to_string(), @@ -175,7 +177,9 @@ impl Slider { if let Some(mut div) = rdom.get_mut(self.post_cursor_div) { let node_type = div.node_type_mut(); - let NodeTypeMut::Element(mut element) = node_type else { panic!("input must be an element") }; + let NodeTypeMut::Element(mut element) = node_type else { + panic!("input must be an element") + }; element.set_attribute( OwnedAttributeDiscription { name: "width".to_string(), @@ -259,7 +263,9 @@ impl CustomElement for Slider { fn create(mut root: dioxus_native_core::real_dom::NodeMut) -> Self { let node_type = root.node_type(); - let NodeType::Element(el) = &*node_type else { panic!("input must be an element") }; + let NodeType::Element(el) = &*node_type else { + panic!("input must be an element") + }; let value = el.attributes.get(&OwnedAttributeDiscription { name: "value".to_string(), @@ -390,7 +396,9 @@ impl CustomElement for Slider { AttributeMask::All => { { let node_type = root.node_type_mut(); - let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") }; + let NodeTypeMut::Element(mut el) = node_type else { + panic!("input must be an element") + }; self.update_value_attr(&el); self.update_size_attr(&mut el); self.update_max_attr(&el); @@ -403,7 +411,9 @@ impl CustomElement for Slider { AttributeMask::Some(attrs) => { { let node_type = root.node_type_mut(); - let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") }; + let NodeTypeMut::Element(mut el) = node_type else { + panic!("input must be an element") + }; if attrs.contains("width") || attrs.contains("height") { self.update_size_attr(&mut el); } diff --git a/packages/rink/src/widgets/text_like.rs b/packages/rink/src/widgets/text_like.rs index b5874c776..6521073d0 100644 --- a/packages/rink/src/widgets/text_like.rs +++ b/packages/rink/src/widgets/text_like.rs @@ -143,19 +143,25 @@ impl TextLike { if let Some(mut text) = rdom.get_mut(self.pre_cursor_text) { let node_type = text.node_type_mut(); - let NodeTypeMut::Text(mut text) = node_type else { panic!("input must be an element") }; + let NodeTypeMut::Text(mut text) = node_type else { + panic!("input must be an element") + }; *text.text_mut() = self.controller.display_text(text_before_first_cursor); } if let Some(mut text) = rdom.get_mut(self.highlighted_text) { let node_type = text.node_type_mut(); - let NodeTypeMut::Text(mut text) = node_type else { panic!("input must be an element") }; + let NodeTypeMut::Text(mut text) = node_type else { + panic!("input must be an element") + }; *text.text_mut() = self.controller.display_text(text_highlighted); } if let Some(mut text) = rdom.get_mut(self.post_cursor_text) { let node_type = text.node_type_mut(); - let NodeTypeMut::Text(mut text) = node_type else { panic!("input must be an element") }; + let NodeTypeMut::Text(mut text) = node_type else { + panic!("input must be an element") + }; *text.text_mut() = self.controller.display_text(text_after_second_cursor); } @@ -288,7 +294,9 @@ impl CustomElement for fn create(mut root: dioxus_native_core::real_dom::NodeMut) -> Self { let node_type = root.node_type(); - let NodeType::Element(el) = &*node_type else { panic!("input must be an element") }; + let NodeType::Element(el) = &*node_type else { + panic!("input must be an element") + }; let value = el .attributes @@ -370,7 +378,9 @@ impl CustomElement for AttributeMask::All => { { let node_type = root.node_type_mut(); - let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") }; + let NodeTypeMut::Element(mut el) = node_type else { + panic!("input must be an element") + }; self.update_value_attr(&el); self.update_size_attr(&mut el); self.update_max_width_attr(&el); @@ -381,7 +391,9 @@ impl CustomElement for AttributeMask::Some(attrs) => { { let node_type = root.node_type_mut(); - let NodeTypeMut::Element(mut el) = node_type else { panic!("input must be an element") }; + let NodeTypeMut::Element(mut el) = node_type else { + panic!("input must be an element") + }; if attrs.contains("width") || attrs.contains("height") { self.update_size_attr(&mut el); } diff --git a/packages/router-macro/src/lib.rs b/packages/router-macro/src/lib.rs index abbbf4355..f8c9cff1d 100644 --- a/packages/router-macro/src/lib.rs +++ b/packages/router-macro/src/lib.rs @@ -31,11 +31,11 @@ mod segment; /// Route Segments: /// 1. Static Segments: "/static" /// 2. Dynamic Segments: "/:dynamic" (where dynamic has a type that is FromStr in all child Variants) -/// 3. Catch all Segments: "/:...segments" (where segments has a type that is FromSegments in all child Variants) +/// 3. Catch all Segments: "/:..segments" (where segments has a type that is FromSegments in all child Variants) /// 4. Query Segments: "/?:query" (where query has a type that is FromQuery in all child Variants) /// /// Routes are matched: -/// 1. By there specificity this order: Query Routes ("/?:query"), Static Routes ("/route"), Dynamic Routes ("/:route"), Catch All Routes ("/:...route") +/// 1. By there specificity this order: Query Routes ("/?:query"), Static Routes ("/route"), Dynamic Routes ("/:route"), Catch All Routes ("/:..route") /// 2. By the order they are defined in the enum /// /// All features: @@ -369,13 +369,13 @@ impl RouteEnum { let (exclude, layout): (bool, Layout) = attr.parse_args_with(parser)?; if exclude { - let Some(layout_index) = - layouts.iter().position(|l| l.comp == layout.comp) else { - return Err(syn::Error::new( - Span::call_site(), - "Attempted to exclude a layout that does not exist", - )); - }; + let Some(layout_index) = layouts.iter().position(|l| l.comp == layout.comp) + else { + return Err(syn::Error::new( + Span::call_site(), + "Attempted to exclude a layout that does not exist", + )); + }; excluded.push(LayoutId(layout_index)); } else { let layout_index = layouts.len(); diff --git a/packages/router-macro/src/route.rs b/packages/router-macro/src/route.rs index 329a7c132..ad8c74857 100644 --- a/packages/router-macro/src/route.rs +++ b/packages/router-macro/src/route.rs @@ -126,7 +126,7 @@ impl Route { None => { return Err(syn::Error::new_spanned( variant.clone(), - "Routable variants with a #[child(...)] attribute must have a field named \"child\" or a field with a #[child] attribute", + "Routable variants with a #[child(..)] attribute must have a field named \"child\" or a field with a #[child] attribute", )); } } @@ -134,14 +134,14 @@ impl Route { _ => { return Err(syn::Error::new_spanned( variant.clone(), - "Routable variants with a #[child(...)] attribute must have named fields", + "Routable variants with a #[child(..)] attribute must have named fields", )) } } } else { return Err(syn::Error::new_spanned( variant.clone(), - "Routable variants must either have a #[route(...)] attribute or a #[child(...)] attribute", + "Routable variants must either have a #[route(..)] attribute or a #[child(..)] attribute", )); } } diff --git a/packages/router-macro/src/segment.rs b/packages/router-macro/src/segment.rs index 30b9e7e10..2f7455a3f 100644 --- a/packages/router-macro/src/segment.rs +++ b/packages/router-macro/src/segment.rs @@ -150,10 +150,10 @@ pub fn parse_route_segments<'a>( while let Some(segment) = iterator.next() { if let Some(segment) = segment.strip_prefix(':') { - let spread = segment.starts_with("..."); + let spread = segment.starts_with(".."); let ident = if spread { - segment[3..].to_string() + segment[2..].to_string() } else { segment.to_string() }; diff --git a/packages/signals/src/lib.rs b/packages/signals/src/lib.rs index 4e950e0dc..5b6a740cf 100644 --- a/packages/signals/src/lib.rs +++ b/packages/signals/src/lib.rs @@ -10,8 +10,6 @@ mod rt; use dioxus_core::ScopeState; pub use rt::*; -use crate::rt::claim_rt; - pub fn use_init_signal_rt(cx: &ScopeState) { cx.use_hook(|| { let rt = claim_rt(cx.schedule_update_any()); @@ -96,11 +94,7 @@ impl std::ops::Deref for Signal { impl std::clone::Clone for Signal { fn clone(&self) -> Self { - Self { - t: PhantomData, - id: self.id, - rt: self.rt, - } + *self } } diff --git a/packages/web/src/rehydrate.rs b/packages/web/src/rehydrate.rs index 8af7da16d..5cc9ea57e 100644 --- a/packages/web/src/rehydrate.rs +++ b/packages/web/src/rehydrate.rs @@ -90,7 +90,7 @@ impl WebsysDom { // make sure we set the root node ids even if the node is not dynamic set_node( hydrated, - vnode.root_ids.get(i).ok_or(VNodeNotInitialized)?, + *vnode.root_ids.borrow().get(i).ok_or(VNodeNotInitialized)?, current_child.clone()?, ); diff --git a/playwright-tests/fullstack/src/main.rs b/playwright-tests/fullstack/src/main.rs index f7cc2e039..cd6089c91 100644 --- a/playwright-tests/fullstack/src/main.rs +++ b/playwright-tests/fullstack/src/main.rs @@ -20,7 +20,7 @@ fn main() { { // Start hot reloading hot_reload_init!(dioxus_hot_reload::Config::new().with_rebuild_callback(|| { - execute::shell("dioxus build --features web") + execute::shell("dx build --features web") .spawn() .unwrap() .wait() diff --git a/package-lock.json b/playwright-tests/package-lock.json similarity index 76% rename from package-lock.json rename to playwright-tests/package-lock.json index 348b505d4..4f1b932ac 100644 --- a/package-lock.json +++ b/playwright-tests/package-lock.json @@ -9,23 +9,23 @@ "version": "1.0.0", "license": "ISC", "devDependencies": { - "@playwright/test": "^1.34.3" + "@playwright/test": "^1.36.1" } }, "node_modules/@playwright/test": { - "version": "1.34.3", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.34.3.tgz", - "integrity": "sha512-zPLef6w9P6T/iT6XDYG3mvGOqOyb6eHaV9XtkunYs0+OzxBtrPAAaHotc0X+PJ00WPPnLfFBTl7mf45Mn8DBmw==", + "version": "1.36.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.36.1.tgz", + "integrity": "sha512-YK7yGWK0N3C2QInPU6iaf/L3N95dlGdbsezLya4n0ZCh3IL7VgPGxC6Gnznh9ApWdOmkJeleT2kMTcWPRZvzqg==", "dev": true, "dependencies": { "@types/node": "*", - "playwright-core": "1.34.3" + "playwright-core": "1.36.1" }, "bin": { "playwright": "cli.js" }, "engines": { - "node": ">=14" + "node": ">=16" }, "optionalDependencies": { "fsevents": "2.3.2" @@ -52,15 +52,15 @@ } }, "node_modules/playwright-core": { - "version": "1.34.3", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.34.3.tgz", - "integrity": "sha512-2pWd6G7OHKemc5x1r1rp8aQcpvDh7goMBZlJv6Co5vCNLVcQJdhxRL09SGaY6HcyHH9aT4tiynZabMofVasBYw==", + "version": "1.36.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.36.1.tgz", + "integrity": "sha512-7+tmPuMcEW4xeCL9cp9KxmYpQYHKkyjwoXRnoeTowaeNat8PoBMk/HwCYhqkH2fRkshfKEOiVus/IhID2Pg8kg==", "dev": true, "bin": { "playwright-core": "cli.js" }, "engines": { - "node": ">=14" + "node": ">=16" } } } diff --git a/package.json b/playwright-tests/package.json similarity index 90% rename from package.json rename to playwright-tests/package.json index ceb38db89..31d444420 100644 --- a/package.json +++ b/playwright-tests/package.json @@ -12,6 +12,6 @@ "author": "", "license": "ISC", "devDependencies": { - "@playwright/test": "^1.34.3" + "@playwright/test": "^1.36.1" } } diff --git a/playwright-tests/playwright-report/index.html b/playwright-tests/playwright-report/index.html new file mode 100644 index 000000000..71730368d --- /dev/null +++ b/playwright-tests/playwright-report/index.html @@ -0,0 +1,62 @@ + + + + + + + + + Playwright Test Report + + + + +

+ + + + \ No newline at end of file diff --git a/playwright.config.js b/playwright-tests/playwright.config.js similarity index 88% rename from playwright.config.js rename to playwright-tests/playwright.config.js index b0531efe8..54fe810a7 100644 --- a/playwright.config.js +++ b/playwright-tests/playwright.config.js @@ -12,7 +12,7 @@ const path = require("path"); * @see https://playwright.dev/docs/test-configuration */ module.exports = defineConfig({ - testDir: "./playwright-tests", + testDir: ".", /* Run tests in files in parallel */ fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ @@ -81,16 +81,16 @@ module.exports = defineConfig({ stdout: "pipe", }, { - cwd: path.join(process.cwd(), "playwright-tests", "web"), - command: "dioxus serve", + cwd: path.join(process.cwd(), "web"), + command: "cargo run --package dioxus-cli -- serve", port: 8080, timeout: 10 * 60 * 1000, reuseExistingServer: !process.env.CI, stdout: "pipe", }, { - cwd: path.join(process.cwd(), 'playwright-tests', 'fullstack'), - command: 'dioxus build --features web --release\ncargo run --release --features ssr --no-default-features', + cwd: path.join(process.cwd(), 'fullstack'), + command: 'cargo run --package dioxus-cli -- build --features web --release\ncargo run --release --features ssr', port: 3333, timeout: 10 * 60 * 1000, reuseExistingServer: !process.env.CI, From f55375b8ce0fa0220a2c4d49029c936b4cb8c164 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 19 Jul 2023 16:57:43 -0700 Subject: [PATCH 9/9] simplify fullstack hello world examples --- .../fullstack/examples/axum-hello-world/src/main.rs | 10 ++-------- .../fullstack/examples/salvo-hello-world/src/main.rs | 10 ++-------- .../fullstack/examples/warp-hello-world/src/main.rs | 10 ++-------- 3 files changed, 6 insertions(+), 24 deletions(-) diff --git a/packages/fullstack/examples/axum-hello-world/src/main.rs b/packages/fullstack/examples/axum-hello-world/src/main.rs index 8a460c685..a5724f8ee 100644 --- a/packages/fullstack/examples/axum-hello-world/src/main.rs +++ b/packages/fullstack/examples/axum-hello-world/src/main.rs @@ -22,14 +22,8 @@ fn app(cx: Scope) -> Element { } fn Child(cx: Scope) -> Element { - let state = use_server_future(cx, (), |()| async move { - loop { - if let Ok(res) = get_server_data().await { - break res; - } - } - })? - .value(); + let state = + use_server_future(cx, (), |()| async move { get_server_data().await.unwrap() })?.value(); let mut count = use_state(cx, || 0); let text = use_state(cx, || "...".to_string()); diff --git a/packages/fullstack/examples/salvo-hello-world/src/main.rs b/packages/fullstack/examples/salvo-hello-world/src/main.rs index d9dcea86f..09c68e1f4 100644 --- a/packages/fullstack/examples/salvo-hello-world/src/main.rs +++ b/packages/fullstack/examples/salvo-hello-world/src/main.rs @@ -22,14 +22,8 @@ fn app(cx: Scope) -> Element { } fn Child(cx: Scope) -> Element { - let state = use_server_future(cx, (), |()| async move { - loop { - if let Ok(res) = get_server_data().await { - break res; - } - } - })? - .value(); + let state = + use_server_future(cx, (), |()| async move { get_server_data().await.unwrap() })?.value(); let mut count = use_state(cx, || 0); let text = use_state(cx, || "...".to_string()); diff --git a/packages/fullstack/examples/warp-hello-world/src/main.rs b/packages/fullstack/examples/warp-hello-world/src/main.rs index d9dcea86f..09c68e1f4 100644 --- a/packages/fullstack/examples/warp-hello-world/src/main.rs +++ b/packages/fullstack/examples/warp-hello-world/src/main.rs @@ -22,14 +22,8 @@ fn app(cx: Scope) -> Element { } fn Child(cx: Scope) -> Element { - let state = use_server_future(cx, (), |()| async move { - loop { - if let Ok(res) = get_server_data().await { - break res; - } - } - })? - .value(); + let state = + use_server_future(cx, (), |()| async move { get_server_data().await.unwrap() })?.value(); let mut count = use_state(cx, || 0); let text = use_state(cx, || "...".to_string());