Documentation Index
Fetch the complete documentation index at: https://mintlify.com/DioxusLabs/dioxus/llms.txt
Use this file to discover all available pages before exploring further.
Hydration is the process where the client-side Dioxus application takes over the server-rendered HTML, attaching event listeners and making the page interactive without re-rendering.
What is Hydration?
When a page is server-side rendered:
- Server: Renders component tree to HTML and embeds state
- Browser: Displays static HTML immediately
- WASM loads: Client code downloads and initializes
- Hydration: Client “hydrates” the DOM, attaching interactivity
use dioxus::prelude::*;
fn app() -> Element {
let mut count = use_signal(|| 0);
rsx! {
// Server renders: <button>Count: 0</button>
// Client hydrates: attaches onclick handler
button {
onclick: move |_| count += 1,
"Count: {count}"
}
}
}
How Hydration Works
Server-Side
- Render Components: VirtualDom renders to HTML
- Collect State: Hooks like
use_signal, use_server_future serialize their data
- Embed Data: State embedded in HTML as base64-encoded CBOR
- Add IDs: Elements get
data-node-id attributes
<!DOCTYPE html>
<html>
<body>
<div id="main">
<button data-node-id="1">Count: 0</button>
</div>
<script>
window.__DIOXUS_HYDRATION_DATA__ = "<base64-encoded-data>";
</script>
<script src="/assets/main.js"></script>
</body>
</html>
Client-Side
- Parse Hydration Data: Read
window.__DIOXUS_HYDRATION_DATA__
- Create VirtualDom: Build same component tree
- Restore State: Hooks read cached values
- Match Nodes: Map virtual nodes to real DOM
- Attach Handlers: Add event listeners
// On the client, this reads cached data instead of calling the server
let data = use_server_future(|| async {
fetch_data().await // Only runs on server
});
Hydration Context
Dioxus automatically manages a HydrationContext that stores serialized state:
// Server: stores data
context.insert(key, serialized_value);
// Client: retrieves data
let value = context.get(key)?;
You typically don’t interact with this directly - hooks handle it automatically.
Hydration-Aware Hooks
Several hooks are designed for hydration:
use_server_future
Runs on server, caches result for client:
fn user_profile(id: u32) -> Element {
let user = use_server_future(move || async move {
// This runs on the server during SSR
// Result is serialized and sent to client
get_user(id).await
});
rsx! {
match user() {
Some(Ok(user)) => rsx! { h1 { "{user.name}" } },
Some(Err(e)) => rsx! { "Error: {e}" },
None => rsx! { "Loading..." },
}
}
}
On the server:
- Future executes
- Result serialized to hydration data
- HTML rendered with result
On the client:
- Future doesn’t execute
- Result read from hydration data
- No server request needed
use_server_cached
Cache computed values:
fn expensive_component() -> Element {
let result = use_server_cached(move || async move {
// Expensive computation on server
compute_expensive_value().await
});
rsx! { "{result:?}" }
}
use_signal with Initial Value
Signals hydrate their initial values:
fn counter() -> Element {
// Server: initializes to 0, serializes
// Client: reads 0 from hydration data
let mut count = use_signal(|| 0);
rsx! {
button { onclick: move |_| count += 1, "Count: {count}" }
}
}
Hydration Mismatches
Hydration fails if server and client render differently:
Common Causes
❌ Random values
fn bad() -> Element {
let random = rand::random::<u32>(); // Different on server/client!
rsx! { "Random: {random}" }
}
❌ Current time
fn bad() -> Element {
let now = std::time::SystemTime::now(); // Different on server/client!
rsx! { "Time: {now:?}" }
}
❌ Browser APIs
fn bad() -> Element {
// window only exists in browser!
let width = window().inner_width();
rsx! { "Width: {width}" }
}
Solutions
✅ Use effects for client-only code
fn good() -> Element {
let mut time = use_signal(|| "Loading...".to_string());
use_effect(move || {
// Runs only on client after hydration
time.set(format!("{:?}", std::time::SystemTime::now()));
});
rsx! { "Time: {time}" }
}
✅ Feature-gate browser code
fn good() -> Element {
let mut width = use_signal(|| 0);
use_effect(move || {
#[cfg(target_arch = "wasm32")]
{
width.set(window().inner_width());
}
});
rsx! { "Width: {width}px" }
}
✅ Use server data
fn good() -> Element {
let data = use_server_future(|| async {
// Deterministic - same on server and client
Ok("Server data".to_string())
});
rsx! { "{data():?}" }
}
Controlling Hydration
Skip Hydration for Elements
Some content doesn’t need hydration:
rsx! {
// Static content - no interactivity needed
article {
dangerous_inner_html: "<p>Static HTML content</p>"
}
}
Conditional Rendering
Handle differences between server and client:
fn component() -> Element {
let is_server = cfg!(feature = "server");
rsx! {
if is_server {
// Server-only view
div { "Rendered on server" }
} else {
// Client-only view
div { "Rendered on client" }
}
}
}
Testing Hydration
Verify Deterministic Rendering
Ensure server and client produce identical output:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_deterministic() {
let html1 = dioxus_ssr::render_element(rsx! { app {} });
let html2 = dioxus_ssr::render_element(rsx! { app {} });
assert_eq!(html1, html2);
}
}
Manual Hydration Test
#[cfg(test)]
mod tests {
#[test]
fn test_hydration() {
// Render on "server"
let mut server_dom = VirtualDom::new(app);
server_dom.rebuild_in_place();
let server_html = dioxus_ssr::render(&server_dom);
// Simulate client hydration
let mut client_dom = VirtualDom::new(app);
// In a real scenario, hydration data would be passed here
client_dom.rebuild_in_place();
// Compare structures
// (detailed comparison would need access to internals)
}
}
Hydration Data Size
Large state increases HTML size. Minimize hydration data:
// ❌ Bad - serializes large data
fn bad() -> Element {
let huge_data = use_server_future(|| async {
Ok(vec![0u8; 1_000_000]) // 1MB serialized!
});
rsx! { "Data loaded" }
}
// ✅ Good - only serialize what's needed
fn good() -> Element {
let summary = use_server_future(|| async {
let data = load_huge_data().await?;
Ok(data.len()) // Just the count
});
rsx! { "Loaded {summary():?} items" }
}
Hydration Timing
Hydration blocks interactivity. Optimize by:
- Reduce JavaScript size: Smaller WASM = faster hydration
- Use code splitting: Load only what’s needed
- Defer non-critical hydration: Use
use_effect for delayed work
fn app() -> Element {
rsx! {
// Critical content - hydrates immediately
Header {}
MainContent {}
// Non-critical - hydrates in background
LazyFooter {}
}
}
fn LazyFooter() -> Element {
let mut loaded = use_signal(|| false);
use_effect(move || {
// Load after hydration
spawn(async move {
tokio::time::sleep(Duration::from_millis(100)).await;
loaded.set(true);
});
});
if !loaded() {
return rsx! { div { "Loading footer..." } };
}
rsx! { footer { "Footer content" } }
}
Debugging Hydration
Enable Logging
Dioxus logs hydration events:
#[cfg(target_arch = "wasm32")]
fn main() {
// Enable console logging
wasm_logger::init(wasm_logger::Config::default());
dioxus::launch(app);
}
Inspect Hydration Data
View hydration data in the browser console:
// In browser console
console.log(window.__DIOXUS_HYDRATION_DATA__);
Check Node IDs
Inspect data-node-id attributes in the rendered HTML to verify hydration markers are present.
Advanced Patterns
Partial Hydration
Hydrate only interactive parts:
fn app() -> Element {
rsx! {
// Static content - no hydration
article { dangerous_inner_html: "<p>Static content</p>" }
// Interactive - needs hydration
div { InteractiveComponent {} }
}
}
Progressive Hydration
Hydrate in priority order:
fn app() -> Element {
rsx! {
// High priority - hydrate first
CriticalComponent {}
// Low priority - hydrate later
Suspense {
fallback: |_| rsx! { "Loading..." },
NonCriticalComponent {}
}
}
}
Streaming with Hydration
Stream HTML while hydrating earlier parts:
fn app() -> Element {
rsx! {
div { "Header" }
Suspense {
fallback: |_| rsx! { "Loading..." },
SlowComponent {} // Streams in later
}
}
}
See examples/07-fullstack/streaming.rs.
Best Practices
- Keep rendering deterministic - same inputs produce same outputs
- Use
use_effect for client-only code - browser APIs, timers, randomness
- Minimize hydration data - only serialize what’s needed
- Test both server and client - ensure consistent rendering
- Use server futures wisely - cache expensive operations
- Handle hydration errors gracefully - provide fallbacks
- Monitor hydration performance - use browser dev tools
Common Issues
Hydration Mismatch Errors
Error: Hydration mismatch - server and client rendered different content
Solution: Ensure deterministic rendering. Check for:
- Random values
- Current time
- Browser APIs used during initial render
Missing Hydration Data
Error: Hydration data not found for key
Solution: Ensure server is rendering with hydration enabled:
let mut renderer = Renderer::new();
renderer.pre_render = true; // Enable hydration markers
Slow Hydration
Solution:
- Reduce WASM bundle size
- Split code
- Defer non-critical hydration
- Use streaming SSR
Examples
examples/07-fullstack/fullstack_hello_world.rs - Basic hydration
examples/07-fullstack/server_functions.rs - Server data hydration
examples/playwright-tests/fullstack-hydration-order - Hydration order tests
examples/07-fullstack/streaming.rs - Streaming with hydration