From b88f14e0f4c988011251602dbda164269a47c663 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 24 Jan 2025 09:02:38 -0600 Subject: [PATCH 1/5] start hydration documentation --- .../0.6/src/guides/fullstack/hydration.md | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 docs-src/0.6/src/guides/fullstack/hydration.md diff --git a/docs-src/0.6/src/guides/fullstack/hydration.md b/docs-src/0.6/src/guides/fullstack/hydration.md new file mode 100644 index 000000000..cb4d45a99 --- /dev/null +++ b/docs-src/0.6/src/guides/fullstack/hydration.md @@ -0,0 +1,155 @@ +# Hydration + +Dioxus fullstack renders the initial HTML on the server and then continues updating that HTML on the client. Hydration lets the client pick up where the server left off. Most of the time, you shouldn't need to think about hydration, but there are a few cases where you might run into hydration issues. To better understand hydration, we'll walk through a simple example: + +```rust +# use dioxus::prelude::*; +# async fn fetch_weather() -> Result { +# todo!() +# } +fn Weather() -> Element { + let weather = use_server_future(fetch_weather)?; + + rsx! { + div { + "{weather:?}" + } + button { + onclick: move |_| weather.restart(), + "Refetch" + } + } +} +``` + +## Rendering the initial HTML + +When the server receives a request to render the `Weather` component, it will: +1) Run the component and wait until all server futures are resolved +2) Serializes any non-deterministic data (like the `weather` future) to hydrate on the client +3) Renders the HTML + +[![](https://mermaid.ink/img/pako:eNpdkDFTwzAMhf-KT3M70HbKwELhGMqSdAIziFhNfI2lnGzDQa__HZfk4Iq1-D1_ejr5BK04ggoOg3y0PWoy-61lE_Nbpzj2Jt68WGhI30lN4x2ZmtiReu4svBZwPs4rtckLm13958ZVaa4zmzsJozBxumqK6ynb4-C_yMxTHnLKSvGa3FyCfiabx_3Tbn4sxsTElVkub0vgLNeT3FieChYQSAN6V1Y9XSALqadAFqpydahHC5bPhcOcpPnkFqqkmRagkrseqgMOsag8Oky09Vh-J_y6I_KzSPhH3TufRGfz_A3Ce3PT?type=png)](https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNpdkDFTwzAMhf-KT3M70HbKwELhGMqSdAIziFhNfI2lnGzDQa__HZfk4Iq1-D1_ejr5BK04ggoOg3y0PWoy-61lE_Nbpzj2Jt68WGhI30lN4x2ZmtiReu4svBZwPs4rtckLm13958ZVaa4zmzsJozBxumqK6ynb4-C_yMxTHnLKSvGa3FyCfiabx_3Tbn4sxsTElVkub0vgLNeT3FieChYQSAN6V1Y9XSALqadAFqpydahHC5bPhcOcpPnkFqqkmRagkrseqgMOsag8Oky09Vh-J_y6I_KzSPhH3TufRGfz_A3Ce3PT) + +Once the server finishes rendering, it will send this structure to the client serialized into the HTML: + +[![](https://mermaid.ink/img/pako:eNqFUcFKAzEQ_ZUwh57agy22sAUFqaCgF1sQNCLTZLYbupss2VmLlv67s92luoo4uSRv3nvzhuzBBEuQQJqHnckwslottFdVvd5ELDNVjZ81LCk6zN0HWbVARg0vQpGyLpJhF7xaXbVIU_5MJCmxyV53hJxR7ATkbc96Irxb71i81c3q_u4_3ybKIOe5dW-DDc9P9GPzXJr7bl5yeeg3p51yXTOLa_Amd2b722QmvAdKI1XZH5mb3R57W7VVjb_dJ1_KY241Gl1IQjWQJB02bbGZ9u2BIRQUC3RWPmPfkDTIkII0JHK1GLcatD8ID2sOy3dvIOFY0xBiqDcZJCnmlbzq0iLTwqEELk5oif4phOIH69o6DrEDD5_uGqQ1?type=png)](https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNqFUcFKAzEQ_ZUwh57agy22sAUFqaCgF1sQNCLTZLYbupss2VmLlv67s92luoo4uSRv3nvzhuzBBEuQQJqHnckwslottFdVvd5ELDNVjZ81LCk6zN0HWbVARg0vQpGyLpJhF7xaXbVIU_5MJCmxyV53hJxR7ATkbc96Irxb71i81c3q_u4_3ybKIOe5dW-DDc9P9GPzXJr7bl5yeeg3p51yXTOLa_Amd2b722QmvAdKI1XZH5mb3R57W7VVjb_dJ1_KY241Gl1IQjWQJB02bbGZ9u2BIRQUC3RWPmPfkDTIkII0JHK1GLcatD8ID2sOy3dvIOFY0xBiqDcZJCnmlbzq0iLTwqEELk5oif4phOIH69o6DrEDD5_uGqQ1) + +## Hydrating on the client + +Once the client receives the initial HTML, it hydrates the HTML by rerunning each component and linking each element the server rendered to the corresponding element on the client. Rerunning each component lets the client re-construct some non-serializable state like event handlers and kick off any client side logic like `use_effect` and `use_future`. + +It will follow these steps: +1) Deserialize the serer future data from the server +2) Run the component with the deserialized data. All server futures are immediately resolved with the deserialized data from the server. +3) Hydrate the HTML sent from the server. This adds all event handlers and links the html nodes to the component so they can be modified later + +[![](https://mermaid.ink/img/pako:eNpdkLFuAjEMhl_F8gxDgemGLlyrDnThmNp0SC-Gi7g4JydpRRHvXsOdkFpnif__s534jG10hBXu-_jddlYy7GrDkMrnQezQQXp4N7juPXGGxjuCl5MT63Nkgx8Kajgv1GYfGTbbUblGWmphTYnE297_EDQkXyTwXHIRSvfqG7tQdlsY1jEMkXXWX3ul9m1u1vm7183kErsRSkuYzx-1zZQuxnRleDw4w0ASrHf60_MVMpg7CmSw0quzcjRo-KKcLTk2J26xylJohhLLocNqb_ukWRmcvqH2VpcT7upg-S3G8I96crolmcTLL4RBdIg?type=png)](https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNpdkLFuAjEMhl_F8gxDgemGLlyrDnThmNp0SC-Gi7g4JydpRRHvXsOdkFpnif__s534jG10hBXu-_jddlYy7GrDkMrnQezQQXp4N7juPXGGxjuCl5MT63Nkgx8Kajgv1GYfGTbbUblGWmphTYnE297_EDQkXyTwXHIRSvfqG7tQdlsY1jEMkXXWX3ul9m1u1vm7183kErsRSkuYzx-1zZQuxnRleDw4w0ASrHf60_MVMpg7CmSw0quzcjRo-KKcLTk2J26xylJohhLLocNqb_ukWRmcvqH2VpcT7upg-S3G8I96crolmcTLL4RBdIg) + +## Hydration errors + +For hydration to work, **the component must render exactly the same thing on the client and the server**. If it doesn't, you might see an error like this: + +``` +Uncaught TypeError: Cannot set properties of undefined (setting 'textContent') +at RawInterpreter.run (yourwasm-hash.js:1:12246) +``` + +Or this: + +``` +Error deserializing data: +Semantic(None, "invalid type: floating point `1.2`, expected integer") +This type was serialized on the server at src/main.rs:11:5 with the type name f64. The client failed to deserialize the type i32 at /path/to/server_future.rs +``` + +To avoid hydration errors, make sure you put any non-deterministic data in a `use_server_future`, `use_server_cached` or effect. For example, if you need +to render a random number on your page, you can use `use_server_cached` to cache the random number on the server and then use it on the client: + +```rust +fn app() -> Element { + // ❌ The random number will be different on the client and the server + let random: u8 = use_hook(|| rand::random()); + // ✅ The same random number will be serialized on the server and deserialized on the client + let random: u8 = use_server_cached(|| rand::random()); + + let mut count = use_signal(|| random); + + rsx! { + button { + onclick: move |_| count += 1, + "{count}" + } + for i in 0..count() { + div { + "{i}" + } + } + } +} +``` + +Or if you need render some data from a server future, you need to use `use_server_future` to serialize the data instead of waiting for the (non-deterministic) amount of time `use_resource(...).suspend()?` takes: + +```rust +#[server] +async fn random_server_function() -> Result { + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + Ok(rand::random()) +} + +fn app() -> Element { + // ❌ The server function result may be finished on the server, but pending on the client + let random: u8 = use_resource(|| random_server_function()).suspend()?().unwrap_or_default(); + // ✅ Once the server function is resolved on the server, it will be sent to the client + let random: u8 = use_server_future(|| random_server_function())?().unwrap().unwrap_or_default(); + + let mut count = use_signal(|| random); + + rsx! { + button { + onclick: move |_| count += 1, + "{count}" + } + for i in 0..count() { + div { + "{i}" + } + } + } +} +``` + +Finally, if you need to grab some data that is only available on the client, make sure you get it inside of a +`use_effect` hook which runs after the component has been hydrated: + +```rust +fn app() -> Element { + // ❌ Using a different value client side before hydration will cause hydration issues + // because the server rendered the html with another value + let storage = use_signal(|| { + #[cfg(feature = "server")] + return None; + let window = web_sys::window().unwrap(); + let local_storage = window.local_storage().unwrap().unwrap(); + local_storage.set_item("count", "1").unwrap(); + local_storage.get_item("count").unwrap() + }); + // ✅ Changing the value inside of an effect is fine because effects run after hydration + let mut storage = use_signal(|| None); + use_effect(move || { + let window = web_sys::window().unwrap(); + let local_storage = window.local_storage().unwrap().unwrap(); + local_storage.set_item("count", "1").unwrap(); + storage.set(local_storage.get_item("count").unwrap()); + }); + + rsx! { + for item in storage() { + div { + "The count is {item}" + } + } + } +} +``` From 5118538631237c0c8154c992bccc5a64efc47783 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 24 Jan 2025 09:21:06 -0600 Subject: [PATCH 2/5] Add a section about side effects in use_server_future --- .../0.6/src/guides/fullstack/hydration.md | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/docs-src/0.6/src/guides/fullstack/hydration.md b/docs-src/0.6/src/guides/fullstack/hydration.md index cb4d45a99..5e997c4fa 100644 --- a/docs-src/0.6/src/guides/fullstack/hydration.md +++ b/docs-src/0.6/src/guides/fullstack/hydration.md @@ -63,6 +63,8 @@ Semantic(None, "invalid type: floating point `1.2`, expected integer") This type was serialized on the server at src/main.rs:11:5 with the type name f64. The client failed to deserialize the type i32 at /path/to/server_future.rs ``` +### Non-deterministic data with server cached + To avoid hydration errors, make sure you put any non-deterministic data in a `use_server_future`, `use_server_cached` or effect. For example, if you need to render a random number on your page, you can use `use_server_cached` to cache the random number on the server and then use it on the client: @@ -89,7 +91,9 @@ fn app() -> Element { } ``` -Or if you need render some data from a server future, you need to use `use_server_future` to serialize the data instead of waiting for the (non-deterministic) amount of time `use_resource(...).suspend()?` takes: +### Async loading with server futures + +If you need render some data from a server future, you need to use `use_server_future` to serialize the data instead of waiting for the (non-deterministic) amount of time `use_resource(...).suspend()?` takes: ```rust #[server] @@ -120,7 +124,9 @@ fn app() -> Element { } ``` -Finally, if you need to grab some data that is only available on the client, make sure you get it inside of a +### Client only data with effects + +If you need to grab some data that is only available on the client, make sure you get it inside of a `use_effect` hook which runs after the component has been hydrated: ```rust @@ -153,3 +159,23 @@ fn app() -> Element { } } ``` + +### Avoid side effects in server cached hooks + +The dioxus fullstack specific hooks `use_server_cached` and `use_server_future` don't run the same on the server and the client. The code you run inside these hooks cannot have side effects because those side effects cannot be serialized: + +```rust +fn app() -> Element { + // ❌ The state of the signal cannot be serialized on the server + let storage = use_signal(|| None); + use_server_future(|| async move { + storage.set(Some(server_future().await)); + })?; + // ✅ The value returned from use_server_future will be serialized on the server and hydrated on the client + let storage = use_server_cached(|| async move { + server_future().await + })?; + + panic!() +} +``` From 8237655fea0fc7e2e00e2b7be2be9b24673c6cfa Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 24 Jan 2025 14:56:59 -0600 Subject: [PATCH 3/5] Check all hydration code snippets --- docs-src/0.6/src/SUMMARY.md | 3 +- .../0.6/src/guides/fullstack/hydration.md | 123 +--- src/doc_examples/hydration.rs | 140 +++++ src/docs/router_06.rs | 593 ++++++++++++------ 4 files changed, 567 insertions(+), 292 deletions(-) create mode 100644 src/doc_examples/hydration.rs diff --git a/docs-src/0.6/src/SUMMARY.md b/docs-src/0.6/src/SUMMARY.md index 988b87da9..c7bc8e22e 100644 --- a/docs-src/0.6/src/SUMMARY.md +++ b/docs-src/0.6/src/SUMMARY.md @@ -53,6 +53,7 @@ - [APIs](guides/mobile/apis.md) - [Streaming and SSR](guides/ssr.md) - [Fullstack](guides/fullstack/index.md) + - [Hydration](guides/fullstack/hydration.md) - [Managing Dependencies](guides/fullstack/managing_dependencies.md) - [Server Functions](guides/fullstack/server_functions.md) - [Extractors](guides/fullstack/extractors.md) @@ -63,7 +64,7 @@ - [Anti-patterns](cookbook/antipatterns.md) - [Error Handling](cookbook/error_handling.md) - [Integrations](cookbook/integrations/index.md) - - [Logging](cookbook/integrations/logging.md) + - [Logging](cookbook/integrations/logging.md)o - [Internationalization](cookbook/integrations/internationalization.md) - [State Management](cookbook/state/index.md) - [External State](cookbook/state/external/index.md) diff --git a/docs-src/0.6/src/guides/fullstack/hydration.md b/docs-src/0.6/src/guides/fullstack/hydration.md index 5e997c4fa..dbcb5458f 100644 --- a/docs-src/0.6/src/guides/fullstack/hydration.md +++ b/docs-src/0.6/src/guides/fullstack/hydration.md @@ -3,31 +3,16 @@ Dioxus fullstack renders the initial HTML on the server and then continues updating that HTML on the client. Hydration lets the client pick up where the server left off. Most of the time, you shouldn't need to think about hydration, but there are a few cases where you might run into hydration issues. To better understand hydration, we'll walk through a simple example: ```rust -# use dioxus::prelude::*; -# async fn fetch_weather() -> Result { -# todo!() -# } -fn Weather() -> Element { - let weather = use_server_future(fetch_weather)?; - - rsx! { - div { - "{weather:?}" - } - button { - onclick: move |_| weather.restart(), - "Refetch" - } - } -} +{{#include src/doc_examples/hydration.rs:hydration_intro}} ``` ## Rendering the initial HTML When the server receives a request to render the `Weather` component, it will: -1) Run the component and wait until all server futures are resolved -2) Serializes any non-deterministic data (like the `weather` future) to hydrate on the client -3) Renders the HTML + +1. Run the component and wait until all server futures are resolved +2. Serializes any non-deterministic data (like the `weather` future) to hydrate on the client +3. Renders the HTML [![](https://mermaid.ink/img/pako:eNpdkDFTwzAMhf-KT3M70HbKwELhGMqSdAIziFhNfI2lnGzDQa__HZfk4Iq1-D1_ejr5BK04ggoOg3y0PWoy-61lE_Nbpzj2Jt68WGhI30lN4x2ZmtiReu4svBZwPs4rtckLm13958ZVaa4zmzsJozBxumqK6ynb4-C_yMxTHnLKSvGa3FyCfiabx_3Tbn4sxsTElVkub0vgLNeT3FieChYQSAN6V1Y9XSALqadAFqpydahHC5bPhcOcpPnkFqqkmRagkrseqgMOsag8Oky09Vh-J_y6I_KzSPhH3TufRGfz_A3Ce3PT?type=png)](https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNpdkDFTwzAMhf-KT3M70HbKwELhGMqSdAIziFhNfI2lnGzDQa__HZfk4Iq1-D1_ejr5BK04ggoOg3y0PWoy-61lE_Nbpzj2Jt68WGhI30lN4x2ZmtiReu4svBZwPs4rtckLm13958ZVaa4zmzsJozBxumqK6ynb4-C_yMxTHnLKSvGa3FyCfiabx_3Tbn4sxsTElVkub0vgLNeT3FieChYQSAN6V1Y9XSALqadAFqpydahHC5bPhcOcpPnkFqqkmRagkrseqgMOsag8Oky09Vh-J_y6I_KzSPhH3TufRGfz_A3Ce3PT) @@ -40,9 +25,10 @@ Once the server finishes rendering, it will send this structure to the client se Once the client receives the initial HTML, it hydrates the HTML by rerunning each component and linking each element the server rendered to the corresponding element on the client. Rerunning each component lets the client re-construct some non-serializable state like event handlers and kick off any client side logic like `use_effect` and `use_future`. It will follow these steps: -1) Deserialize the serer future data from the server -2) Run the component with the deserialized data. All server futures are immediately resolved with the deserialized data from the server. -3) Hydrate the HTML sent from the server. This adds all event handlers and links the html nodes to the component so they can be modified later + +1. Deserialize the serer future data from the server +2. Run the component with the deserialized data. All server futures are immediately resolved with the deserialized data from the server. +3. Hydrate the HTML sent from the server. This adds all event handlers and links the html nodes to the component so they can be modified later [![](https://mermaid.ink/img/pako:eNpdkLFuAjEMhl_F8gxDgemGLlyrDnThmNp0SC-Gi7g4JydpRRHvXsOdkFpnif__s534jG10hBXu-_jddlYy7GrDkMrnQezQQXp4N7juPXGGxjuCl5MT63Nkgx8Kajgv1GYfGTbbUblGWmphTYnE297_EDQkXyTwXHIRSvfqG7tQdlsY1jEMkXXWX3ul9m1u1vm7183kErsRSkuYzx-1zZQuxnRleDw4w0ASrHf60_MVMpg7CmSw0quzcjRo-KKcLTk2J26xylJohhLLocNqb_ukWRmcvqH2VpcT7upg-S3G8I96crolmcTLL4RBdIg?type=png)](https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNpdkLFuAjEMhl_F8gxDgemGLlyrDnThmNp0SC-Gi7g4JydpRRHvXsOdkFpnif__s534jG10hBXu-_jddlYy7GrDkMrnQezQQXp4N7juPXGGxjuCl5MT63Nkgx8Kajgv1GYfGTbbUblGWmphTYnE297_EDQkXyTwXHIRSvfqG7tQdlsY1jEMkXXWX3ul9m1u1vm7183kErsRSkuYzx-1zZQuxnRleDw4w0ASrHf60_MVMpg7CmSw0quzcjRo-KKcLTk2J26xylJohhLLocNqb_ukWRmcvqH2VpcT7upg-S3G8I96crolmcTLL4RBdIg) @@ -69,26 +55,7 @@ To avoid hydration errors, make sure you put any non-deterministic data in a `us to render a random number on your page, you can use `use_server_cached` to cache the random number on the server and then use it on the client: ```rust -fn app() -> Element { - // ❌ The random number will be different on the client and the server - let random: u8 = use_hook(|| rand::random()); - // ✅ The same random number will be serialized on the server and deserialized on the client - let random: u8 = use_server_cached(|| rand::random()); - - let mut count = use_signal(|| random); - - rsx! { - button { - onclick: move |_| count += 1, - "{count}" - } - for i in 0..count() { - div { - "{i}" - } - } - } -} +{{#include src/doc_examples/hydration.rs:server_cached}} ``` ### Async loading with server futures @@ -96,32 +63,7 @@ fn app() -> Element { If you need render some data from a server future, you need to use `use_server_future` to serialize the data instead of waiting for the (non-deterministic) amount of time `use_resource(...).suspend()?` takes: ```rust -#[server] -async fn random_server_function() -> Result { - tokio::time::sleep(std::time::Duration::from_millis(100)).await; - Ok(rand::random()) -} - -fn app() -> Element { - // ❌ The server function result may be finished on the server, but pending on the client - let random: u8 = use_resource(|| random_server_function()).suspend()?().unwrap_or_default(); - // ✅ Once the server function is resolved on the server, it will be sent to the client - let random: u8 = use_server_future(|| random_server_function())?().unwrap().unwrap_or_default(); - - let mut count = use_signal(|| random); - - rsx! { - button { - onclick: move |_| count += 1, - "{count}" - } - for i in 0..count() { - div { - "{i}" - } - } - } -} +{{#include src/doc_examples/hydration.rs:server_future}} ``` ### Client only data with effects @@ -130,34 +72,7 @@ If you need to grab some data that is only available on the client, make sure yo `use_effect` hook which runs after the component has been hydrated: ```rust -fn app() -> Element { - // ❌ Using a different value client side before hydration will cause hydration issues - // because the server rendered the html with another value - let storage = use_signal(|| { - #[cfg(feature = "server")] - return None; - let window = web_sys::window().unwrap(); - let local_storage = window.local_storage().unwrap().unwrap(); - local_storage.set_item("count", "1").unwrap(); - local_storage.get_item("count").unwrap() - }); - // ✅ Changing the value inside of an effect is fine because effects run after hydration - let mut storage = use_signal(|| None); - use_effect(move || { - let window = web_sys::window().unwrap(); - let local_storage = window.local_storage().unwrap().unwrap(); - local_storage.set_item("count", "1").unwrap(); - storage.set(local_storage.get_item("count").unwrap()); - }); - - rsx! { - for item in storage() { - div { - "The count is {item}" - } - } - } -} +{{#include src/doc_examples/hydration.rs:effects}} ``` ### Avoid side effects in server cached hooks @@ -165,17 +80,5 @@ fn app() -> Element { The dioxus fullstack specific hooks `use_server_cached` and `use_server_future` don't run the same on the server and the client. The code you run inside these hooks cannot have side effects because those side effects cannot be serialized: ```rust -fn app() -> Element { - // ❌ The state of the signal cannot be serialized on the server - let storage = use_signal(|| None); - use_server_future(|| async move { - storage.set(Some(server_future().await)); - })?; - // ✅ The value returned from use_server_future will be serialized on the server and hydrated on the client - let storage = use_server_cached(|| async move { - server_future().await - })?; - - panic!() -} +{{#include src/doc_examples/hydration.rs:server_hook_side_effects}} ``` diff --git a/src/doc_examples/hydration.rs b/src/doc_examples/hydration.rs new file mode 100644 index 000000000..d61aad8d6 --- /dev/null +++ b/src/doc_examples/hydration.rs @@ -0,0 +1,140 @@ +mod hydration_intro { + use dioxus::prelude::*; + async fn fetch_weather() -> Result { + todo!() + } + // ANCHOR: hydration_intro + fn Weather() -> Element { + let mut weather = use_server_future(fetch_weather)?; + + rsx! { + div { + "{weather:?}" + } + button { + onclick: move |_| weather.restart(), + "Refetch" + } + } + } + // ANCHOR_END: hydration_intro +} + +mod server_cached { + use dioxus::prelude::*; + + fn app() -> Element { + // ANCHOR: server_cached + // ❌ The random number will be different on the client and the server + let random: u8 = use_hook(|| rand::random()); + // ✅ The same random number will be serialized on the server and deserialized on the client + let random: u8 = use_server_cached(|| rand::random()); + // ANCHOR_END: server_cached + + let mut count = use_signal(|| random); + + rsx! { + button { + onclick: move |_| count += 1, + "{count}" + } + for i in 0..count() { + div { + "{i}" + } + } + } + } +} + +mod server_future { + use dioxus::prelude::*; + + #[server] + async fn random_server_function() -> Result { + tokio::time::sleep(std::time::Duration::from_millis(100)).await; + Ok(rand::random()) + } + + fn app() -> Element { + // ANCHOR: server_future + // ❌ The server function result may be finished on the server, but pending on the client + let random: u8 = use_resource(|| random_server_function()).suspend()?().unwrap_or_default(); + // ✅ Once the server function is resolved on the server, it will be sent to the client + let random: u8 = use_server_future(|| random_server_function())?() + .unwrap() + .unwrap_or_default(); + // ANCHOR_END: server_future + + let mut count = use_signal(|| random); + + rsx! { + button { + onclick: move |_| count += 1, + "{count}" + } + for i in 0..count() { + div { + "{i}" + } + } + } + } +} + +mod effects { + use dioxus::prelude::*; + + fn app() -> Element { + // ANCHOR: effects + // ❌ Using a different value client side before hydration will cause hydration issues + // because the server rendered the html with another value + let mut storage = use_signal(|| { + #[cfg(feature = "server")] + return None; + let window = web_sys::window().unwrap(); + let local_storage = window.local_storage().unwrap().unwrap(); + local_storage.set_item("count", "1").unwrap(); + local_storage.get_item("count").unwrap() + }); + // ✅ Changing the value inside of an effect is fine because effects run after hydration + let mut storage = use_signal(|| None); + use_effect(move || { + let window = web_sys::window().unwrap(); + let local_storage = window.local_storage().unwrap().unwrap(); + local_storage.set_item("count", "1").unwrap(); + storage.set(local_storage.get_item("count").unwrap()); + }); + // ANCHOR_END: effects + + rsx! { + for item in storage() { + div { + "The count is {item}" + } + } + } + } +} + +mod server_hook_side_effects { + use dioxus::prelude::*; + + async fn server_future() -> Result { + todo!() + } + + fn app() -> Element { + // ANCHOR: server_hook_side_effects + // ❌ The state of the signal cannot be serialized on the server + let mut storage = use_signal(|| None); + use_server_future(move || async move { + storage.set(Some(server_future().await)); + })?; + // ✅ The value returned from use_server_future will be serialized on the server and hydrated on the client + let storage = use_server_future(|| async move { server_future().await })?; + // ANCHOR_END: server_hook_side_effects + + panic!() + } +} diff --git a/src/docs/router_06.rs b/src/docs/router_06.rs index 3206d264d..fd5e49e60 100644 --- a/src/docs/router_06.rs +++ b/src/docs/router_06.rs @@ -100,6 +100,8 @@ pub enum BookRoute { GuidesSsr {}, #[route("/guides/fullstack/")] GuidesFullstackIndex {}, + #[route("/guides/fullstack/hydration")] + GuidesFullstackHydration {}, #[route("/guides/fullstack/managing_dependencies")] GuidesFullstackManagingDependencies {}, #[route("/guides/fullstack/server_functions")] @@ -253,62 +255,63 @@ impl BookRoute { BookRoute::GuidesMobileApis {} => use_mdbook::mdbook_shared::PageId(42usize), BookRoute::GuidesSsr {} => use_mdbook::mdbook_shared::PageId(43usize), BookRoute::GuidesFullstackIndex {} => use_mdbook::mdbook_shared::PageId(44usize), + BookRoute::GuidesFullstackHydration {} => use_mdbook::mdbook_shared::PageId(45usize), BookRoute::GuidesFullstackManagingDependencies {} => { - use_mdbook::mdbook_shared::PageId(45usize) + use_mdbook::mdbook_shared::PageId(46usize) } BookRoute::GuidesFullstackServerFunctions {} => { - use_mdbook::mdbook_shared::PageId(46usize) + use_mdbook::mdbook_shared::PageId(47usize) } - BookRoute::GuidesFullstackExtractors {} => use_mdbook::mdbook_shared::PageId(47usize), - BookRoute::GuidesFullstackMiddleware {} => use_mdbook::mdbook_shared::PageId(48usize), + BookRoute::GuidesFullstackExtractors {} => use_mdbook::mdbook_shared::PageId(48usize), + BookRoute::GuidesFullstackMiddleware {} => use_mdbook::mdbook_shared::PageId(49usize), BookRoute::GuidesFullstackAuthentication {} => { - use_mdbook::mdbook_shared::PageId(49usize) - } - BookRoute::GuidesFullstackRouting {} => use_mdbook::mdbook_shared::PageId(50usize), - BookRoute::CookbookPublishing {} => use_mdbook::mdbook_shared::PageId(51usize), - BookRoute::CookbookAntipatterns {} => use_mdbook::mdbook_shared::PageId(52usize), - BookRoute::CookbookErrorHandling {} => use_mdbook::mdbook_shared::PageId(53usize), - BookRoute::CookbookIntegrationsIndex {} => use_mdbook::mdbook_shared::PageId(54usize), - BookRoute::CookbookIntegrationsLogging {} => use_mdbook::mdbook_shared::PageId(55usize), + use_mdbook::mdbook_shared::PageId(50usize) + } + BookRoute::GuidesFullstackRouting {} => use_mdbook::mdbook_shared::PageId(51usize), + BookRoute::CookbookPublishing {} => use_mdbook::mdbook_shared::PageId(52usize), + BookRoute::CookbookAntipatterns {} => use_mdbook::mdbook_shared::PageId(53usize), + BookRoute::CookbookErrorHandling {} => use_mdbook::mdbook_shared::PageId(54usize), + BookRoute::CookbookIntegrationsIndex {} => use_mdbook::mdbook_shared::PageId(55usize), + BookRoute::CookbookIntegrationsLogging {} => use_mdbook::mdbook_shared::PageId(56usize), BookRoute::CookbookIntegrationsInternationalization {} => { - use_mdbook::mdbook_shared::PageId(56usize) + use_mdbook::mdbook_shared::PageId(57usize) } - BookRoute::CookbookStateIndex {} => use_mdbook::mdbook_shared::PageId(57usize), - BookRoute::CookbookStateExternalIndex {} => use_mdbook::mdbook_shared::PageId(58usize), + BookRoute::CookbookStateIndex {} => use_mdbook::mdbook_shared::PageId(58usize), + BookRoute::CookbookStateExternalIndex {} => use_mdbook::mdbook_shared::PageId(59usize), BookRoute::CookbookStateCustomHooksIndex {} => { - use_mdbook::mdbook_shared::PageId(59usize) - } - BookRoute::CookbookBundling {} => use_mdbook::mdbook_shared::PageId(60usize), - BookRoute::CookbookTesting {} => use_mdbook::mdbook_shared::PageId(61usize), - BookRoute::CookbookTailwind {} => use_mdbook::mdbook_shared::PageId(62usize), - BookRoute::CookbookOptimizing {} => use_mdbook::mdbook_shared::PageId(63usize), - BookRoute::MigrationIndex {} => use_mdbook::mdbook_shared::PageId(64usize), - BookRoute::ReferenceIndex {} => use_mdbook::mdbook_shared::PageId(65usize), - BookRoute::ReferenceHotreload {} => use_mdbook::mdbook_shared::PageId(66usize), - BookRoute::ReferenceRsx {} => use_mdbook::mdbook_shared::PageId(67usize), - BookRoute::ReferenceComponents {} => use_mdbook::mdbook_shared::PageId(68usize), - BookRoute::ReferenceComponentProps {} => use_mdbook::mdbook_shared::PageId(69usize), - BookRoute::ReferenceEventHandlers {} => use_mdbook::mdbook_shared::PageId(70usize), - BookRoute::ReferenceHooks {} => use_mdbook::mdbook_shared::PageId(71usize), - BookRoute::ReferenceUserInput {} => use_mdbook::mdbook_shared::PageId(72usize), - BookRoute::ReferenceContext {} => use_mdbook::mdbook_shared::PageId(73usize), - BookRoute::ReferenceDynamicRendering {} => use_mdbook::mdbook_shared::PageId(74usize), - BookRoute::ReferenceRouter {} => use_mdbook::mdbook_shared::PageId(75usize), - BookRoute::ReferenceUseResource {} => use_mdbook::mdbook_shared::PageId(76usize), - BookRoute::ReferenceUseCoroutine {} => use_mdbook::mdbook_shared::PageId(77usize), - BookRoute::ReferenceSpawn {} => use_mdbook::mdbook_shared::PageId(78usize), - BookRoute::ContributingIndex {} => use_mdbook::mdbook_shared::PageId(79usize), + use_mdbook::mdbook_shared::PageId(60usize) + } + BookRoute::CookbookBundling {} => use_mdbook::mdbook_shared::PageId(61usize), + BookRoute::CookbookTesting {} => use_mdbook::mdbook_shared::PageId(62usize), + BookRoute::CookbookTailwind {} => use_mdbook::mdbook_shared::PageId(63usize), + BookRoute::CookbookOptimizing {} => use_mdbook::mdbook_shared::PageId(64usize), + BookRoute::MigrationIndex {} => use_mdbook::mdbook_shared::PageId(65usize), + BookRoute::ReferenceIndex {} => use_mdbook::mdbook_shared::PageId(66usize), + BookRoute::ReferenceHotreload {} => use_mdbook::mdbook_shared::PageId(67usize), + BookRoute::ReferenceRsx {} => use_mdbook::mdbook_shared::PageId(68usize), + BookRoute::ReferenceComponents {} => use_mdbook::mdbook_shared::PageId(69usize), + BookRoute::ReferenceComponentProps {} => use_mdbook::mdbook_shared::PageId(70usize), + BookRoute::ReferenceEventHandlers {} => use_mdbook::mdbook_shared::PageId(71usize), + BookRoute::ReferenceHooks {} => use_mdbook::mdbook_shared::PageId(72usize), + BookRoute::ReferenceUserInput {} => use_mdbook::mdbook_shared::PageId(73usize), + BookRoute::ReferenceContext {} => use_mdbook::mdbook_shared::PageId(74usize), + BookRoute::ReferenceDynamicRendering {} => use_mdbook::mdbook_shared::PageId(75usize), + BookRoute::ReferenceRouter {} => use_mdbook::mdbook_shared::PageId(76usize), + BookRoute::ReferenceUseResource {} => use_mdbook::mdbook_shared::PageId(77usize), + BookRoute::ReferenceUseCoroutine {} => use_mdbook::mdbook_shared::PageId(78usize), + BookRoute::ReferenceSpawn {} => use_mdbook::mdbook_shared::PageId(79usize), + BookRoute::ContributingIndex {} => use_mdbook::mdbook_shared::PageId(80usize), BookRoute::ContributingProjectStructure {} => { - use_mdbook::mdbook_shared::PageId(80usize) + use_mdbook::mdbook_shared::PageId(81usize) } BookRoute::ContributingGuidingPrinciples {} => { - use_mdbook::mdbook_shared::PageId(81usize) + use_mdbook::mdbook_shared::PageId(82usize) } - BookRoute::ContributingRoadmap {} => use_mdbook::mdbook_shared::PageId(82usize), - BookRoute::CliIndex {} => use_mdbook::mdbook_shared::PageId(83usize), - BookRoute::CliCreating {} => use_mdbook::mdbook_shared::PageId(84usize), - BookRoute::CliConfigure {} => use_mdbook::mdbook_shared::PageId(85usize), - BookRoute::CliTranslate {} => use_mdbook::mdbook_shared::PageId(86usize), + BookRoute::ContributingRoadmap {} => use_mdbook::mdbook_shared::PageId(83usize), + BookRoute::CliIndex {} => use_mdbook::mdbook_shared::PageId(84usize), + BookRoute::CliCreating {} => use_mdbook::mdbook_shared::PageId(85usize), + BookRoute::CliConfigure {} => use_mdbook::mdbook_shared::PageId(86usize), + BookRoute::CliTranslate {} => use_mdbook::mdbook_shared::PageId(87usize), } } } @@ -2067,6 +2070,61 @@ pub static LAZY_BOOK: use_mdbook::Lazy dioxus::prelude::Element { a { href: "#support", class: "header", "Support" } } p { - "The Rust ecosystem for mobile is still in its infancy. Mobile is a 1st-class target for Dioxus apps, but there are very few packages that are battle-tested and ready to use." + "The Rust ecosystem for mobile continues to mature, with Dioxus offering strong support for mobile applications. Mobile is a first-class target for Dioxus apps, with a robust WebView implementation that supports CSS animations and transparency effects." } p { - "Mobile apps are rendered with either the platform's WebView or experimentally with " - a { href: "https://github.com/DioxusLabs/blitz", "WGPU" } - ". WebView doesn't support animations, transparency, and native widgets." + "Mobile apps are rendered with either the platform's WebView or experimentally with WGPU. While native Android animations and widgets aren't currently supported, CSS-based animations and styling provide a powerful alternative." } p { - "Mobile support is currently best suited for CRUD-style apps, ideally for internal teams who need to develop quickly but don't care much about animations or native widgets." + "Mobile support is well-suited for most application types, from business tools to consumer apps, making it an excellent choice for teams looking to build cross-platform applications with a single codebase." } h2 { id: "getting-set-up", a { href: "#getting-set-up", class: "header", "Getting Set up" } @@ -10502,6 +10569,170 @@ pub fn GuidesFullstackIndex() -> dioxus::prelude::Element { } } #[component(no_case_check)] +pub fn GuidesFullstackHydration() -> dioxus::prelude::Element { + use dioxus::prelude::*; + rsx! { + h1 { id: "hydration", + a { href: "#hydration", class: "header", "Hydration" } + } + p { + "Dioxus fullstack renders the initial HTML on the server and then continues updating that HTML on the client. Hydration lets the client pick up where the server left off. Most of the time, you shouldn't need to think about hydration, but there are a few cases where you might run into hydration issues. To better understand hydration, we'll walk through a simple example:" + } + CodeBlock { + contents: "
\nfn Weather() -> Element {{\n    let weather = use_server_future(fetch_weather)?;\n\n    rsx! {{\n        div {{\n            "{{weather:?}}"\n        }}\n        button {{\n            onclick: move |_| weather.restart(),\n            "Refetch"\n        }}\n    }}\n}}
\n", + name: "hydration.rs".to_string(), + } + h2 { id: "rendering-the-initial-html", + a { href: "#rendering-the-initial-html", class: "header", "Rendering the initial HTML" } + } + p { + "When the server receives a request to render the " + code { "Weather" } + " component, it will:" + } + ol { + li { "Run the component and wait until all server futures are resolved" } + li { + "Serializes any non-deterministic data (like the " + code { "weather" } + " future) to hydrate on the client" + } + li { "Renders the HTML" } + } + p { + a { href: "https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNpdkDFTwzAMhf-KT3M70HbKwELhGMqSdAIziFhNfI2lnGzDQa__HZfk4Iq1-D1_ejr5BK04ggoOg3y0PWoy-61lE_Nbpzj2Jt68WGhI30lN4x2ZmtiReu4svBZwPs4rtckLm13958ZVaa4zmzsJozBxumqK6ynb4-C_yMxTHnLKSvGa3FyCfiabx_3Tbn4sxsTElVkub0vgLNeT3FieChYQSAN6V1Y9XSALqadAFqpydahHC5bPhcOcpPnkFqqkmRagkrseqgMOsag8Oky09Vh-J_y6I_KzSPhH3TufRGfz_A3Ce3PT", + img { + src: "https://mermaid.ink/img/pako:eNpdkDFTwzAMhf-KT3M70HbKwELhGMqSdAIziFhNfI2lnGzDQa__HZfk4Iq1-D1_ejr5BK04ggoOg3y0PWoy-61lE_Nbpzj2Jt68WGhI30lN4x2ZmtiReu4svBZwPs4rtckLm13958ZVaa4zmzsJozBxumqK6ynb4-C_yMxTHnLKSvGa3FyCfiabx_3Tbn4sxsTElVkub0vgLNeT3FieChYQSAN6V1Y9XSALqadAFqpydahHC5bPhcOcpPnkFqqkmRagkrseqgMOsag8Oky09Vh-J_y6I_KzSPhH3TufRGfz_A3Ce3PT?type=png", + alt: "", + title: "", + } + } + } + p { + "Once the server finishes rendering, it will send this structure to the client serialized into the HTML:" + } + p { + a { href: "https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNqFUcFKAzEQ_ZUwh57agy22sAUFqaCgF1sQNCLTZLYbupss2VmLlv67s92luoo4uSRv3nvzhuzBBEuQQJqHnckwslottFdVvd5ELDNVjZ81LCk6zN0HWbVARg0vQpGyLpJhF7xaXbVIU_5MJCmxyV53hJxR7ATkbc96Irxb71i81c3q_u4_3ybKIOe5dW-DDc9P9GPzXJr7bl5yeeg3p51yXTOLa_Amd2b722QmvAdKI1XZH5mb3R57W7VVjb_dJ1_KY241Gl1IQjWQJB02bbGZ9u2BIRQUC3RWPmPfkDTIkII0JHK1GLcatD8ID2sOy3dvIOFY0xBiqDcZJCnmlbzq0iLTwqEELk5oif4phOIH69o6DrEDD5_uGqQ1", + img { + src: "https://mermaid.ink/img/pako:eNqFUcFKAzEQ_ZUwh57agy22sAUFqaCgF1sQNCLTZLYbupss2VmLlv67s92luoo4uSRv3nvzhuzBBEuQQJqHnckwslottFdVvd5ELDNVjZ81LCk6zN0HWbVARg0vQpGyLpJhF7xaXbVIU_5MJCmxyV53hJxR7ATkbc96Irxb71i81c3q_u4_3ybKIOe5dW-DDc9P9GPzXJr7bl5yeeg3p51yXTOLa_Amd2b722QmvAdKI1XZH5mb3R57W7VVjb_dJ1_KY241Gl1IQjWQJB02bbGZ9u2BIRQUC3RWPmPfkDTIkII0JHK1GLcatD8ID2sOy3dvIOFY0xBiqDcZJCnmlbzq0iLTwqEELk5oif4phOIH69o6DrEDD5_uGqQ1?type=png", + alt: "", + title: "", + } + } + } + h2 { id: "hydrating-on-the-client", + a { href: "#hydrating-on-the-client", class: "header", "Hydrating on the client" } + } + p { + "Once the client receives the initial HTML, it hydrates the HTML by rerunning each component and linking each element the server rendered to the corresponding element on the client. Rerunning each component lets the client re-construct some non-serializable state like event handlers and kick off any client side logic like " + code { "use_effect" } + " and " + code { "use_future" } + "." + } + p { "It will follow these steps:" } + ol { + li { "Deserialize the serer future data from the server" } + li { + "Run the component with the deserialized data. All server futures are immediately resolved with the deserialized data from the server." + } + li { + "Hydrate the HTML sent from the server. This adds all event handlers and links the html nodes to the component so they can be modified later" + } + } + p { + a { href: "https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNpdkLFuAjEMhl_F8gxDgemGLlyrDnThmNp0SC-Gi7g4JydpRRHvXsOdkFpnif__s534jG10hBXu-_jddlYy7GrDkMrnQezQQXp4N7juPXGGxjuCl5MT63Nkgx8Kajgv1GYfGTbbUblGWmphTYnE297_EDQkXyTwXHIRSvfqG7tQdlsY1jEMkXXWX3ul9m1u1vm7183kErsRSkuYzx-1zZQuxnRleDw4w0ASrHf60_MVMpg7CmSw0quzcjRo-KKcLTk2J26xylJohhLLocNqb_ukWRmcvqH2VpcT7upg-S3G8I96crolmcTLL4RBdIg", + img { + src: "https://mermaid.ink/img/pako:eNpdkLFuAjEMhl_F8gxDgemGLlyrDnThmNp0SC-Gi7g4JydpRRHvXsOdkFpnif__s534jG10hBXu-_jddlYy7GrDkMrnQezQQXp4N7juPXGGxjuCl5MT63Nkgx8Kajgv1GYfGTbbUblGWmphTYnE297_EDQkXyTwXHIRSvfqG7tQdlsY1jEMkXXWX3ul9m1u1vm7183kErsRSkuYzx-1zZQuxnRleDw4w0ASrHf60_MVMpg7CmSw0quzcjRo-KKcLTk2J26xylJohhLLocNqb_ukWRmcvqH2VpcT7upg-S3G8I96crolmcTLL4RBdIg?type=png", + alt: "", + title: "", + } + } + } + h2 { id: "hydration-errors", + a { href: "#hydration-errors", class: "header", "Hydration errors" } + } + p { + "For hydration to work, " + strong { "the component must render exactly the same thing on the client and the server" } + ". If it doesn't, you might see an error like this:" + } + CodeBlock { contents: "
\nUncaught TypeError: Cannot set properties of undefined (setting 'textContent')\nat RawInterpreter.run (yourwasm-hash.js:1:12246)
\n" } + p { "Or this:" } + CodeBlock { + contents: "
\nError deserializing data:\nSemantic(None, "invalid type: floating point `1.2`, expected integer")\nThis type was serialized on the server at src/main.rs:11:5 with the type name f64. The client failed to deserialize the type i32 at /path/to/server_future.rs
\n", + } + h3 { id: "non-deterministic-data-with-server-cached", + a { + href: "#non-deterministic-data-with-server-cached", + class: "header", + "Non-deterministic data with server cached" + } + } + p { + "To avoid hydration errors, make sure you put any non-deterministic data in a " + code { "use_server_future" } + ", " + code { "use_server_cached" } + " or effect. For example, if you need" + code { "use_server_cached" } + " to cache the random number on the server and then use it on the client:" + } + CodeBlock { + contents: "
\n// ❌ The random number will be different on the client and the server\nlet random: u8 = use_hook(|| rand::random());\n// ✅ The same random number will be serialized on the server and deserialized on the client\nlet random: u8 = use_server_cached(|| rand::random());
\n", + name: "hydration.rs".to_string(), + } + h3 { id: "async-loading-with-server-futures", + a { href: "#async-loading-with-server-futures", class: "header", + "Async loading with server futures" + } + } + p { + "If you need render some data from a server future, you need to use " + code { "use_server_future" } + " to serialize the data instead of waiting for the (non-deterministic) amount of time " + code { "use_resource(...).suspend()?" } + " takes:" + } + CodeBlock { + contents: "
\n// ❌ The server function result may be finished on the server, but pending on the client\nlet random: u8 = use_resource(|| random_server_function()).suspend()?().unwrap_or_default();\n// ✅ Once the server function is resolved on the server, it will be sent to the client\nlet random: u8 = use_server_future(|| random_server_function())?()\n    .unwrap()\n    .unwrap_or_default();
\n", + name: "hydration.rs".to_string(), + } + h3 { id: "client-only-data-with-effects", + a { href: "#client-only-data-with-effects", class: "header", + "Client only data with effects" + } + } + p { + "If you need to grab some data that is only available on the client, make sure you get it inside of a" + code { "use_effect" } + " hook which runs after the component has been hydrated:" + } + CodeBlock { + contents: "
\n// ❌ Using a different value client side before hydration will cause hydration issues\n// because the server rendered the html with another value\nlet storage = use_signal(|| {{\n    #[cfg(feature = "server")]\n    return None;\n    let window = web_sys::window().unwrap();\n    let local_storage = window.local_storage().unwrap().unwrap();\n    local_storage.set_item("count", "1").unwrap();\n    local_storage.get_item("count").unwrap()\n}});\n// ✅ Changing the value inside of an effect is fine because effects run after hydration\nlet mut storage = use_signal(|| None);\nuse_effect(move || {{\n    let window = web_sys::window().unwrap();\n    let local_storage = window.local_storage().unwrap().unwrap();\n    local_storage.set_item("count", "1").unwrap();\n    storage.set(local_storage.get_item("count").unwrap());\n}});
\n", + name: "hydration.rs".to_string(), + } + h3 { id: "avoid-side-effects-in-server-cached-hooks", + a { + href: "#avoid-side-effects-in-server-cached-hooks", + class: "header", + "Avoid side effects in server cached hooks" + } + } + p { + "The dioxus fullstack specific hooks " + code { "use_server_cached" } + " and " + code { "use_server_future" } + " don't run the same on the server and the client. The code you run inside these hooks cannot have side effects because those side effects cannot be serialized:" + } + CodeBlock { + contents: "
\n// ❌ The state of the signal cannot be serialized on the server\nlet storage = use_signal(|| None);\nuse_server_future(|| async move {{\n    storage.set(Some(server_future().await));\n}})?;\n// ✅ The value returned from use_server_future will be serialized on the server and hydrated on the client\nlet storage = use_server_cached(|| async move {{\n    server_future().await\n}})?;
\n", + name: "hydration.rs".to_string(), + } + } +} +#[component(no_case_check)] pub fn GuidesFullstackManagingDependencies() -> dioxus::prelude::Element { use dioxus::prelude::*; rsx! { @@ -15762,7 +15993,7 @@ pub fn CliConfigure() -> dioxus::prelude::Element { } p { "This includes all fields, mandatory or not." } CodeBlock { - contents: "
\n[application]\n\n# App name\nname = "project_name"\n\n# The Dioxus platform to default to\ndefault_platform = "web"\n\n# `build` & `serve` output path\nout_dir = "dist"\n\n# The static resource path\nasset_dir = "public"\n\n[web.app]\n\n# HTML title tag content\ntitle = "project_name"\n\n[web.watcher]\n\n# When watcher is triggered, regenerate the `index.html`\nreload_html = true\n\n# Which files or dirs will be monitored\nwatch_path = ["src", "public"]\n\n# Include style or script assets\n[web.resource]\n\n# CSS style file\nstyle = []\n\n# Javascript code file\nscript = []\n\n[web.resource.dev]\n\n# Same as [web.resource], but for development servers\n\n# CSS style file\nstyle = []\n\n# JavaScript files\nscript = []\n\n[[web.proxy]]\nbackend = "http://localhost:8000/api/"\n\n[bundle]\nidentifier = "com.dioxuslabs"\npublisher = "DioxusLabs"\nicon = "assets/icon.png"
\n", + contents: "
\n[application]\n\n# App name\nname = "project_name"\n\n# `build` & `serve` output path\nout_dir = "dist"\n\n# The static resource path\nasset_dir = "public"\n\n[web.app]\n\n# HTML title tag content\ntitle = "project_name"\n\n[web.watcher]\n\n# When watcher is triggered, regenerate the `index.html`\nreload_html = true\n\n# Which files or dirs will be monitored\nwatch_path = ["src", "public"]\n\n# Include style or script assets\n[web.resource]\n\n# CSS style file\nstyle = []\n\n# Javascript code file\nscript = []\n\n[web.resource.dev]\n\n# Same as [web.resource], but for development servers\n\n# CSS style file\nstyle = []\n\n# JavaScript files\nscript = []\n\n[[web.proxy]]\nbackend = "http://localhost:8000/api/"\n\n[bundle]\nidentifier = "com.dioxuslabs"\npublisher = "DioxusLabs"\nicon = "assets/icon.png"
\n", } } } From b0744d71a4628b4a17716787733542ab65ff5f90 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Fri, 24 Jan 2025 14:57:51 -0600 Subject: [PATCH 4/5] remove typo from summary --- docs-src/0.6/src/SUMMARY.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs-src/0.6/src/SUMMARY.md b/docs-src/0.6/src/SUMMARY.md index c7bc8e22e..ee9875ab7 100644 --- a/docs-src/0.6/src/SUMMARY.md +++ b/docs-src/0.6/src/SUMMARY.md @@ -64,7 +64,7 @@ - [Anti-patterns](cookbook/antipatterns.md) - [Error Handling](cookbook/error_handling.md) - [Integrations](cookbook/integrations/index.md) - - [Logging](cookbook/integrations/logging.md)o + - [Logging](cookbook/integrations/logging.md) - [Internationalization](cookbook/integrations/internationalization.md) - [State Management](cookbook/state/index.md) - [External State](cookbook/state/external/index.md) From 19da056c970a6285e4bfeaa64d16e01991bf9fa6 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Mon, 27 Jan 2025 14:01:34 -0600 Subject: [PATCH 5/5] Finalize hydration guide --- .../0.6/src/guides/fullstack/hydration.md | 27 +++++------ src/docs/router_06.rs | 47 ++++++++++++------- 2 files changed, 42 insertions(+), 32 deletions(-) diff --git a/docs-src/0.6/src/guides/fullstack/hydration.md b/docs-src/0.6/src/guides/fullstack/hydration.md index dbcb5458f..4b30f0b8f 100644 --- a/docs-src/0.6/src/guides/fullstack/hydration.md +++ b/docs-src/0.6/src/guides/fullstack/hydration.md @@ -1,6 +1,6 @@ # Hydration -Dioxus fullstack renders the initial HTML on the server and then continues updating that HTML on the client. Hydration lets the client pick up where the server left off. Most of the time, you shouldn't need to think about hydration, but there are a few cases where you might run into hydration issues. To better understand hydration, we'll walk through a simple example: +In dioxus fullstack, the server renders the initial HTML for improved loading times. This initial version of the page is what most web crawlers and search engines see. After the initial HTML is rendered, the client makes the page interactive and takes over rendering in a process called **hydration**. Most of the time, you shouldn't need to think about hydration, but there are a few things you need to keep in mind to avoid [hydration errors](#hydration-errors). To better understand hydration, let's walk through a simple example: ```rust {{#include src/doc_examples/hydration.rs:hydration_intro}} @@ -8,27 +8,28 @@ Dioxus fullstack renders the initial HTML on the server and then continues updat ## Rendering the initial HTML -When the server receives a request to render the `Weather` component, it will: +When the server receives a request to render the `Weather` component, it renders the page to HTML and serializes some additional data the client needs to hydrate the page. It will follow these steps to render our component: -1. Run the component and wait until all server futures are resolved -2. Serializes any non-deterministic data (like the `weather` future) to hydrate on the client -3. Renders the HTML +1. Run the component +2. Wait until all server futures are resolved +3. Serialize any non-deterministic data (like the `weather` future) for the client +4. Render the HTML [![](https://mermaid.ink/img/pako:eNpdkDFTwzAMhf-KT3M70HbKwELhGMqSdAIziFhNfI2lnGzDQa__HZfk4Iq1-D1_ejr5BK04ggoOg3y0PWoy-61lE_Nbpzj2Jt68WGhI30lN4x2ZmtiReu4svBZwPs4rtckLm13958ZVaa4zmzsJozBxumqK6ynb4-C_yMxTHnLKSvGa3FyCfiabx_3Tbn4sxsTElVkub0vgLNeT3FieChYQSAN6V1Y9XSALqadAFqpydahHC5bPhcOcpPnkFqqkmRagkrseqgMOsag8Oky09Vh-J_y6I_KzSPhH3TufRGfz_A3Ce3PT?type=png)](https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNpdkDFTwzAMhf-KT3M70HbKwELhGMqSdAIziFhNfI2lnGzDQa__HZfk4Iq1-D1_ejr5BK04ggoOg3y0PWoy-61lE_Nbpzj2Jt68WGhI30lN4x2ZmtiReu4svBZwPs4rtckLm13958ZVaa4zmzsJozBxumqK6ynb4-C_yMxTHnLKSvGa3FyCfiabx_3Tbn4sxsTElVkub0vgLNeT3FieChYQSAN6V1Y9XSALqadAFqpydahHC5bPhcOcpPnkFqqkmRagkrseqgMOsag8Oky09Vh-J_y6I_KzSPhH3TufRGfz_A3Ce3PT) -Once the server finishes rendering, it will send this structure to the client serialized into the HTML: +Once the server finishes rendering, it will send this structure to the client as HTML: [![](https://mermaid.ink/img/pako:eNqFUcFKAzEQ_ZUwh57agy22sAUFqaCgF1sQNCLTZLYbupss2VmLlv67s92luoo4uSRv3nvzhuzBBEuQQJqHnckwslottFdVvd5ELDNVjZ81LCk6zN0HWbVARg0vQpGyLpJhF7xaXbVIU_5MJCmxyV53hJxR7ATkbc96Irxb71i81c3q_u4_3ybKIOe5dW-DDc9P9GPzXJr7bl5yeeg3p51yXTOLa_Amd2b722QmvAdKI1XZH5mb3R57W7VVjb_dJ1_KY241Gl1IQjWQJB02bbGZ9u2BIRQUC3RWPmPfkDTIkII0JHK1GLcatD8ID2sOy3dvIOFY0xBiqDcZJCnmlbzq0iLTwqEELk5oif4phOIH69o6DrEDD5_uGqQ1?type=png)](https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNqFUcFKAzEQ_ZUwh57agy22sAUFqaCgF1sQNCLTZLYbupss2VmLlv67s92luoo4uSRv3nvzhuzBBEuQQJqHnckwslottFdVvd5ELDNVjZ81LCk6zN0HWbVARg0vQpGyLpJhF7xaXbVIU_5MJCmxyV53hJxR7ATkbc96Irxb71i81c3q_u4_3ybKIOe5dW-DDc9P9GPzXJr7bl5yeeg3p51yXTOLa_Amd2b722QmvAdKI1XZH5mb3R57W7VVjb_dJ1_KY241Gl1IQjWQJB02bbGZ9u2BIRQUC3RWPmPfkDTIkII0JHK1GLcatD8ID2sOy3dvIOFY0xBiqDcZJCnmlbzq0iLTwqEELk5oif4phOIH69o6DrEDD5_uGqQ1) ## Hydrating on the client -Once the client receives the initial HTML, it hydrates the HTML by rerunning each component and linking each element the server rendered to the corresponding element on the client. Rerunning each component lets the client re-construct some non-serializable state like event handlers and kick off any client side logic like `use_effect` and `use_future`. +When the client receives the initial HTML, it hydrates the HTML by rerunning each component and linking each node that component renders to the node the server rendered. Rerunning each component lets the client re-construct some non-serializable state like event handlers and kick off any client side logic like `use_effect` and `use_future`. It will follow these steps: -1. Deserialize the serer future data from the server +1. Deserialize any non-deterministic data from the server (like the `weather` future) 2. Run the component with the deserialized data. All server futures are immediately resolved with the deserialized data from the server. -3. Hydrate the HTML sent from the server. This adds all event handlers and links the html nodes to the component so they can be modified later +3. Hydrate the HTML sent from the server. This adds all event handlers and links the html nodes to the component so they can be moved or modified later [![](https://mermaid.ink/img/pako:eNpdkLFuAjEMhl_F8gxDgemGLlyrDnThmNp0SC-Gi7g4JydpRRHvXsOdkFpnif__s534jG10hBXu-_jddlYy7GrDkMrnQezQQXp4N7juPXGGxjuCl5MT63Nkgx8Kajgv1GYfGTbbUblGWmphTYnE297_EDQkXyTwXHIRSvfqG7tQdlsY1jEMkXXWX3ul9m1u1vm7183kErsRSkuYzx-1zZQuxnRleDw4w0ASrHf60_MVMpg7CmSw0quzcjRo-KKcLTk2J26xylJohhLLocNqb_ukWRmcvqH2VpcT7upg-S3G8I96crolmcTLL4RBdIg?type=png)](https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNpdkLFuAjEMhl_F8gxDgemGLlyrDnThmNp0SC-Gi7g4JydpRRHvXsOdkFpnif__s534jG10hBXu-_jddlYy7GrDkMrnQezQQXp4N7juPXGGxjuCl5MT63Nkgx8Kajgv1GYfGTbbUblGWmphTYnE297_EDQkXyTwXHIRSvfqG7tQdlsY1jEMkXXWX3ul9m1u1vm7183kErsRSkuYzx-1zZQuxnRleDw4w0ASrHf60_MVMpg7CmSw0quzcjRo-KKcLTk2J26xylJohhLLocNqb_ukWRmcvqH2VpcT7upg-S3G8I96crolmcTLL4RBdIg) @@ -51,8 +52,7 @@ This type was serialized on the server at src/main.rs:11:5 with the type name f6 ### Non-deterministic data with server cached -To avoid hydration errors, make sure you put any non-deterministic data in a `use_server_future`, `use_server_cached` or effect. For example, if you need -to render a random number on your page, you can use `use_server_cached` to cache the random number on the server and then use it on the client: +You must put any non-deterministic data in `use_server_future`, `use_server_cached` or `use_effect` to avoid hydration errors. For example, if you need to render a random number on your page, you can use `use_server_cached` to cache the random number on the server and then use it on the client: ```rust {{#include src/doc_examples/hydration.rs:server_cached}} @@ -68,8 +68,7 @@ If you need render some data from a server future, you need to use `use_server_f ### Client only data with effects -If you need to grab some data that is only available on the client, make sure you get it inside of a -`use_effect` hook which runs after the component has been hydrated: +If you need to grab some data that is only available on the client, make sure you get it inside of a `use_effect` hook which runs after the component has been hydrated: ```rust {{#include src/doc_examples/hydration.rs:effects}} @@ -77,7 +76,7 @@ If you need to grab some data that is only available on the client, make sure yo ### Avoid side effects in server cached hooks -The dioxus fullstack specific hooks `use_server_cached` and `use_server_future` don't run the same on the server and the client. The code you run inside these hooks cannot have side effects because those side effects cannot be serialized: +The dioxus fullstack specific hooks `use_server_cached` and `use_server_future` don't run the same on the server and the client. The server will always run the closure, but the client may not run the closure if the server serialized the result. Because of this, the code you run inside these hooks **cannot have side effects**. If it does, the side effects will not be serialized and it can cause a hydration mismatch error: ```rust {{#include src/doc_examples/hydration.rs:server_hook_side_effects}} diff --git a/src/docs/router_06.rs b/src/docs/router_06.rs index fd5e49e60..11014eff8 100644 --- a/src/docs/router_06.rs +++ b/src/docs/router_06.rs @@ -10576,10 +10576,14 @@ pub fn GuidesFullstackHydration() -> dioxus::prelude::Element { a { href: "#hydration", class: "header", "Hydration" } } p { - "Dioxus fullstack renders the initial HTML on the server and then continues updating that HTML on the client. Hydration lets the client pick up where the server left off. Most of the time, you shouldn't need to think about hydration, but there are a few cases where you might run into hydration issues. To better understand hydration, we'll walk through a simple example:" + "In dioxus fullstack, the server renders the initial HTML for improved loading times. This initial version of the page is what most web crawlers and search engines see. After the initial HTML is rendered, the client takes over rendering the page in a process called " + em { "hydration" } + ". Most of the time, you shouldn't need to think about hydration, but there are a few things you need to keep in mind to avoid " + a { href: "#hydration-errors", "hydration errors" } + ". To better understand hydration, let's walk through a simple example:" } CodeBlock { - contents: "
\nfn Weather() -> Element {{\n    let weather = use_server_future(fetch_weather)?;\n\n    rsx! {{\n        div {{\n            "{{weather:?}}"\n        }}\n        button {{\n            onclick: move |_| weather.restart(),\n            "Refetch"\n        }}\n    }}\n}}
\n", + contents: "
\nfn Weather() -> Element {{\n    let mut weather = use_server_future(fetch_weather)?;\n\n    rsx! {{\n        div {{\n            "{{weather:?}}"\n        }}\n        button {{\n            onclick: move |_| weather.restart(),\n            "Refetch"\n        }}\n    }}\n}}
\n", name: "hydration.rs".to_string(), } h2 { id: "rendering-the-initial-html", @@ -10588,16 +10592,17 @@ pub fn GuidesFullstackHydration() -> dioxus::prelude::Element { p { "When the server receives a request to render the " code { "Weather" } - " component, it will:" + " component, it renders the page to HTML and serializes some additional data the client needs to hydrate the page. It will follow these steps to render our component:" } ol { - li { "Run the component and wait until all server futures are resolved" } + li { "Run the component" } + li { "Wait until all server futures are resolved" } li { - "Serializes any non-deterministic data (like the " + "Serialize any non-deterministic data (like the " code { "weather" } - " future) to hydrate on the client" + " future) for the client" } - li { "Renders the HTML" } + li { "Render the HTML" } } p { a { href: "https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNpdkDFTwzAMhf-KT3M70HbKwELhGMqSdAIziFhNfI2lnGzDQa__HZfk4Iq1-D1_ejr5BK04ggoOg3y0PWoy-61lE_Nbpzj2Jt68WGhI30lN4x2ZmtiReu4svBZwPs4rtckLm13958ZVaa4zmzsJozBxumqK6ynb4-C_yMxTHnLKSvGa3FyCfiabx_3Tbn4sxsTElVkub0vgLNeT3FieChYQSAN6V1Y9XSALqadAFqpydahHC5bPhcOcpPnkFqqkmRagkrseqgMOsag8Oky09Vh-J_y6I_KzSPhH3TufRGfz_A3Ce3PT", @@ -10608,9 +10613,7 @@ pub fn GuidesFullstackHydration() -> dioxus::prelude::Element { } } } - p { - "Once the server finishes rendering, it will send this structure to the client serialized into the HTML:" - } + p { "Once the server finishes rendering, it will send this structure to the client as HTML:" } p { a { href: "https://mermaid-js.github.io/mermaid-live-editor/edit#pako:eNqFUcFKAzEQ_ZUwh57agy22sAUFqaCgF1sQNCLTZLYbupss2VmLlv67s92luoo4uSRv3nvzhuzBBEuQQJqHnckwslottFdVvd5ELDNVjZ81LCk6zN0HWbVARg0vQpGyLpJhF7xaXbVIU_5MJCmxyV53hJxR7ATkbc96Irxb71i81c3q_u4_3ybKIOe5dW-DDc9P9GPzXJr7bl5yeeg3p51yXTOLa_Amd2b722QmvAdKI1XZH5mb3R57W7VVjb_dJ1_KY241Gl1IQjWQJB02bbGZ9u2BIRQUC3RWPmPfkDTIkII0JHK1GLcatD8ID2sOy3dvIOFY0xBiqDcZJCnmlbzq0iLTwqEELk5oif4phOIH69o6DrEDD5_uGqQ1", img { @@ -10624,7 +10627,7 @@ pub fn GuidesFullstackHydration() -> dioxus::prelude::Element { a { href: "#hydrating-on-the-client", class: "header", "Hydrating on the client" } } p { - "Once the client receives the initial HTML, it hydrates the HTML by rerunning each component and linking each element the server rendered to the corresponding element on the client. Rerunning each component lets the client re-construct some non-serializable state like event handlers and kick off any client side logic like " + "When the client receives the initial HTML, it hydrates the HTML by rerunning each component and linking each node that component renders to the node the server rendered. Rerunning each component lets the client re-construct some non-serializable state like event handlers and kick off any client side logic like " code { "use_effect" } " and " code { "use_future" } @@ -10632,12 +10635,16 @@ pub fn GuidesFullstackHydration() -> dioxus::prelude::Element { } p { "It will follow these steps:" } ol { - li { "Deserialize the serer future data from the server" } + li { + "Deserialize any non-deterministic data from the server (like the " + code { "weather" } + " future)" + } li { "Run the component with the deserialized data. All server futures are immediately resolved with the deserialized data from the server." } li { - "Hydrate the HTML sent from the server. This adds all event handlers and links the html nodes to the component so they can be modified later" + "Hydrate the HTML sent from the server. This adds all event handlers and links the html nodes to the component so they can be moved or modified later" } } p { @@ -10674,7 +10681,9 @@ pub fn GuidesFullstackHydration() -> dioxus::prelude::Element { code { "use_server_future" } ", " code { "use_server_cached" } - " or effect. For example, if you need" + " or " + code { "use_effect" } + ". For example, if you need to render a random number on your page, you can use " code { "use_server_cached" } " to cache the random number on the server and then use it on the client:" } @@ -10704,12 +10713,12 @@ pub fn GuidesFullstackHydration() -> dioxus::prelude::Element { } } p { - "If you need to grab some data that is only available on the client, make sure you get it inside of a" + "If you need to grab some data that is only available on the client, make sure you get it inside of a " code { "use_effect" } " hook which runs after the component has been hydrated:" } CodeBlock { - contents: "
\n// ❌ Using a different value client side before hydration will cause hydration issues\n// because the server rendered the html with another value\nlet storage = use_signal(|| {{\n    #[cfg(feature = "server")]\n    return None;\n    let window = web_sys::window().unwrap();\n    let local_storage = window.local_storage().unwrap().unwrap();\n    local_storage.set_item("count", "1").unwrap();\n    local_storage.get_item("count").unwrap()\n}});\n// ✅ Changing the value inside of an effect is fine because effects run after hydration\nlet mut storage = use_signal(|| None);\nuse_effect(move || {{\n    let window = web_sys::window().unwrap();\n    let local_storage = window.local_storage().unwrap().unwrap();\n    local_storage.set_item("count", "1").unwrap();\n    storage.set(local_storage.get_item("count").unwrap());\n}});
\n", + contents: "
\n// ❌ Using a different value client side before hydration will cause hydration issues\n// because the server rendered the html with another value\nlet mut storage = use_signal(|| {{\n    #[cfg(feature = "server")]\n    return None;\n    let window = web_sys::window().unwrap();\n    let local_storage = window.local_storage().unwrap().unwrap();\n    local_storage.set_item("count", "1").unwrap();\n    local_storage.get_item("count").unwrap()\n}});\n// ✅ Changing the value inside of an effect is fine because effects run after hydration\nlet mut storage = use_signal(|| None);\nuse_effect(move || {{\n    let window = web_sys::window().unwrap();\n    let local_storage = window.local_storage().unwrap().unwrap();\n    local_storage.set_item("count", "1").unwrap();\n    storage.set(local_storage.get_item("count").unwrap());\n}});
\n", name: "hydration.rs".to_string(), } h3 { id: "avoid-side-effects-in-server-cached-hooks", @@ -10724,10 +10733,12 @@ pub fn GuidesFullstackHydration() -> dioxus::prelude::Element { code { "use_server_cached" } " and " code { "use_server_future" } - " don't run the same on the server and the client. The code you run inside these hooks cannot have side effects because those side effects cannot be serialized:" + " don't run the same on the server and the client. The server will always run the closure, but the client may not run the closure if the server serialized the result. Because of this, the code you run inside these hooks " + strong { "cannot have side effects" } + ". If it does, the side effects will not be serialized and it can cause a hydration mismatch error: " } CodeBlock { - contents: "
\n// ❌ The state of the signal cannot be serialized on the server\nlet storage = use_signal(|| None);\nuse_server_future(|| async move {{\n    storage.set(Some(server_future().await));\n}})?;\n// ✅ The value returned from use_server_future will be serialized on the server and hydrated on the client\nlet storage = use_server_cached(|| async move {{\n    server_future().await\n}})?;
\n", + contents: "
\n// ❌ The state of the signal cannot be serialized on the server\nlet mut storage = use_signal(|| None);\nuse_server_future(move || async move {{\n    storage.set(Some(server_future().await));\n}})?;\n// ✅ The value returned from use_server_future will be serialized on the server and hydrated on the client\nlet storage = use_server_future(|| async move {{ server_future().await }})?;
\n", name: "hydration.rs".to_string(), } }