`use_server_future` Hydration error
Problem
- main.rs
#[cfg(feature = "web")]
dioxus_web::launch::launch(App, vec![], dioxus_web::Config::new().hydrate(true));
#[cfg(feature = "server")]
dioxus::launch(App);
- component.rs
use_resourceworks properly.use_server_futurecauses hydration error.
#[component]
pub fn CollectionListPage(lang: Language) -> Element {
let navigator = use_navigator();
let mut collections: Signal<Vec<Collection>> = Signal::default();
let mut num_items: Signal<usize> = Signal::default();
let collections = use_resource(move || async move {
match list_collections(None, None, Some(0), Some(100)).await {
Ok(v) => v,
Err(e) => {
tracing::error!("failed to fetch collections: {:?}", e);
vec![]
}
}
})
.value();
// FIXME: it needs to be rendered by the server for SSR.
// let collections = use_server_future(move || async move {
// match list_collections(None, None, Some(0), Some(100)).await {
// Ok(v) => v,
// Err(e) => {
// tracing::error!("failed to fetch collections: {:?}", e);
// vec![]
// }
// }
// })?
// .value();
rsx! {
div {
class: "w-full h-full",
GridLayout {
class: "py-4 lg:py-10",
if let Some(collections) = collections() {
for collection in collections {
ImageCard {
onclick: move |_evt| {
navigator
.push(Route::CollectionDetailPage {
lang: lang.clone(),
agit_id: collection.agit_id.unwrap_or_default(),
collection_id: collection.id,
});
},
image_url: collection.logo.clone(),
p { class: "font-semibold text-2xl pt-4 line-clamp-1", {collection.name.clone()} }
p { class: "text-[#666666] font-medium",
"Item "
span { class: "text-primary ml-1", "{num_items()}" }
}
}
}
}
}
}
}
}
Steps To Reproduce
Steps to reproduce the behavior:
- Refresh on the component route (e.g. http://localhost:8080/ko/collection/list)
- It causes errors and can't navigate to any webpage.
Expected behavior
- Render without errors
- It can navigate to other pages.
Screenshots
Environment:
- Dioxus version:
0.6.0-alpha.2 - Rust version:
rustc 1.80.1 (3f5fd8dd4 2024-08-06) - OS info:
Arch linux 6.11.2 - App platform: fullstack
Questionnaire
- [ ] I'm interested in fixing this myself but don't know where to start
- [ ] I would like to fix and I have a solution
- [ ] I don't have time to fix this right now, but maybe later
#[cfg(feature = "web")] dioxus_web::launch::launch(App, vec![], dioxus_web::Config::new().hydrate(true)); #[cfg(feature = "server")] dioxus::launch(App);
If you are using fullstack, you need to launch fullstack on the web renderer. The fullstack entry point has special logic for hydrating server functions:
LaunchBuilder::new()
.with_cfg(web!{dioxus_web::Config::new().hydrate(true)})
.launch(App)
#[cfg(feature = "web")] dioxus_web::launch::launch(App, vec![], dioxus_web::Config::new().hydrate(true)); #[cfg(feature = "server")] dioxus::launch(App);If you are using fullstack, you need to launch fullstack on the web renderer. The fullstack entry point has special logic for hydrating server functions:
LaunchBuilder::new() .with_cfg(web!{dioxus_web::Config::new().hydrate(true)}) .launch(App)
I've changed main.rs as below
#[cfg(feature = "web")]
dioxus::prelude::LaunchBuilder::new()
.with_cfg(web! {dioxus_web::Config::new().hydrate(true)})
.launch(App);
#[cfg(feature = "server")]
dioxus_aws::launch(App);
However, if uncomment use_server_future, it causes same error when refresh on that page. @ealmloff
As a additional information, the above use_server_future code does work for 0.5 version of dioxus.
Could you provide a minimal reproduction for this issue? I cannot reproduce it with a simple router example with a server future:
use dioxus::prelude::*;
fn main() {
LaunchBuilder::fullstack()
.with_cfg(server_only!(ServeConfig::builder().incremental(
IncrementalRendererConfig::default()
.invalidate_after(std::time::Duration::from_secs(120)),
)))
.launch(app);
}
fn app() -> Element {
rsx! { Router::<Route> {} }
}
#[derive(Clone, Routable, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
enum Route {
#[route("/")]
Home {},
#[route("/blog/:id/")]
Blog { id: i32 },
}
#[component]
fn Blog(id: i32) -> Element {
let future = use_server_future(get_server_data)?;
let future = future.value();
rsx! {
"{future:?}"
Link { to: Route::Home {}, "Go to counter" }
table {
tbody {
for _ in 0..id {
tr {
for _ in 0..id {
td { "hello world!" }
}
}
}
}
}
}
}
#[component]
fn Home() -> Element {
let mut count = use_signal(|| 0);
let mut text = use_signal(|| "...".to_string());
rsx! {
Link { to: Route::Blog { id: count() }, "Go to blog" }
div {
h1 { "High-Five counter: {count}" }
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
button {
onclick: move |_| async move {
if let Ok(data) = get_server_data().await {
println!("Client received: {}", data);
text.set(data.clone());
post_server_data(data).await.unwrap();
}
},
"Run server function!"
}
"Server said: {text}"
}
}
}
#[server(PostServerData)]
async fn post_server_data(data: String) -> Result<(), ServerFnError> {
println!("Server received: {}", data);
Ok(())
}
#[server(GetServerData)]
async fn get_server_data() -> Result<String, ServerFnError> {
Ok("Hello from the server!".to_string())
}
I'm encountering the same problem with my fullstack app using SSR and server functions (alpha.5). But it only happens if I use the new document::Link feature to load CSS asset.
this happens after the first server call completes, so first I see in the log
INFO /home/ochrons/.cargo/registry/src/index.crates.io-6f17d22bba15001f/dioxus-fullstack-0.6.0-alpha.5/src/hooks/server_future.rs:75 First run of use_server_future
then the server is called (with 1sec delay on the server side) then I see the same log line again
INFO /home/ochrons/.cargo/registry/src/index.crates.io-6f17d22bba15001f/dioxus-fullstack-0.6.0-alpha.5/src/hooks/server_future.rs:75 First run of use_server_future
followed by the hydration error
ERROR /home/ochrons/.cargo/registry/src/index.crates.io-6f17d22bba15001f/dioxus-web-0.6.0-alpha.5/src/hydration/deserialize.rs:92 Error deserializing data: Semantic(None, "invalid type: boolean `true`, expected enum")
patch_console.js:1
wasm-bindgen: imported JS function that was not marked as `catch` threw an error: Cannot read properties of undefined (reading 'toString')
Setting a breakpoint at the location of the error reveals following. Error happens on this line due to id being undefined
hydrateNode.setAttribute("data-dioxus-id", id.toString());
it is extracted from a list of ids in
id = ids[parseInt(split[0])]
where ids contains
ids: Uint32Array(3) [1, 2, 3, buffer: ArrayBuffer(12) ...
and split[0] contains the value 172
Now, I don't know why ids list only contains the first three ids in the page, but definitely that is the issue why it crashes. The id 172 refers to a button element on the page.
This is also the first call to hydrate_node function, and it only takes place after the use_server_future is completed on the client side, which is also somewhat odd. This call occurs after the deserialization error is logged, which also happens before the server call is actually made.
I'm encountering the same problem with my fullstack app using SSR and server functions (alpha.5). But it only happens if I use the new
document::Linkfeature to load CSS asset.
Can you share the code that causes that error? It looks like it doesn't effect all fullstack applications with document::Link. This code hydrates correctly:
// [dependencies]
// dioxus = { version = "0.6.0-alpha.5", features = ["fullstack"] }
//
// [features]
// web = ["dioxus/web"]
// server = ["dioxus/server"]
use dioxus::prelude::*;
fn main() {
launch(app);
}
fn app() -> Element {
let server_data = use_server_future(get_server_data)?;
let mut count = use_signal(|| 0);
rsx! {
document::Link {
rel: "stylesheet",
href: asset!("/assets/main.css"),
}
div {
h1 { "High-Five counter: {count}" }
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
"Server said: {server_data:?}"
}
}
}
#[server(GetServerData)]
async fn get_server_data() -> Result<String, ServerFnError> {
Ok("Hello from the server!".to_string())
}
ERROR /home/ochrons/.cargo/registry/src/index.crates.io-6f17d22bba15001f/dioxus-web-0.6.0-alpha.5/src/hydration/deserialize.rs:92 Error deserializing data: Semantic(None, "invalid type: boolean `true`, expected enum") patch_console.js:1 wasm-bindgen: imported JS function that was not marked as `catch` threw an error: Cannot read properties of undefined (reading 'toString')
The javascript errors are likely caused by the deserialization error. If the client gets the wrong data, it cannot pick up the html from the server and it will cause issues when trying to make further updates. That error can also be caused by behavior that is different between the client and server. For example, if the client calls use_server_future, but the server does not it could cause a similar error
@ealmloff currently, all of our code use use_resource instead of use_server_future. I will share minimal code for this issue within 10 days. But I may should invite you my private repository if I can't make a very short code for the hydration error.
@ealmloff here are some relevant snippets from my code, which hopefully are enough to build a minimal reproduction.
Startup code
fn main() {
#[cfg(feature = "web")]
main_client(components::App);
#[cfg(feature = "server")]
main_server(components::App);
}
#[cfg(feature = "web")]
fn main_client(main_element: fn() -> Element) {
dioxus_logger::init(tracing::Level::INFO).expect("failed to init logger");
// Hydrate the application on the client
LaunchBuilder::new()
.with_cfg(web! {dioxus::web::Config::new().hydrate(true)})
.launch(main_element);
}
#[cfg(feature = "server")]
fn main_server(main_element: fn() -> Element) {
tokio::runtime::Runtime::new()
.unwrap()
.block_on(server::start_server(main_element))
.unwrap(); // Handle the Result returned by block_on.
}
Main App
pub fn App() -> Element {
rsx! {
document::Link { rel: "stylesheet", href: asset!("/public/tailwind.css") }
div {
class: "flex flex-col justify-center overflow-hidden py-6 px-2",
"data-theme": "light",
div { class: "mx-auto max-w-2xl", Router::<BaseRoute> {} }
}
}
}
The router
pub enum BaseRoute {
#[nest("/:language")]
#[route("/recipe/:id")]
RecipeView { language: Language, id: i64 },
#[end_nest]
#[route("/")]
Home {},
#[route("/:..route")]
PageNotFound { route: Vec<String> },
}
The view that crashes
#[component]
pub fn RecipeView(id: ReadOnlySignal<i64>, language: ReadOnlySignal<Language>) -> Element {
let mut scale = use_signal(|| 1.0f32);
let mut unit_system = use_signal(|| UnitSystem::Metric);
// get the recipe from the server
let recipe = use_server_future(move || get_recipe(id(), language()))?;
// let recipe = use_resource(move || get_recipe(id(), language())).suspend()?;
match recipe() {
None | Some(Err(_)) => {
rsx! { "Error loading recipes from the server." }
}
Some(Ok(recipe)) => {
// recipe viewing stuff, followed by buttons that cause the hydration error
div { class: "flex flex-row justify-left space-x-3 py-3",
button {
class: "btn btn-primary",
onclick: move |_| scale.set(scale() * 2.0),
"Double"
}
button {
class: "btn btn-secondary",
onclick: move |_| scale.set(scale() * 1.0 / 2.0),
"Halve"
}
button {
class: "btn btn-primary",
onclick: move |_| unit_system.set(UnitSystem::US),
"US"
}
button {
class: "btn btn-secondary",
onclick: move |_| unit_system.set(UnitSystem::Metric),
"Metric"
}
}
The server function
type Result<T> = std::result::Result<T, ServerFnError>;
#[server]
pub async fn get_recipe(id: i64, language: Language) -> Result<Recipe> {
use crate::server::{get_app_state, recipe_transformer::transform_recipe};
use lib_core::model::recipe::RecipeBmc;
println!("get_recipe: id: {}, language: {}", id, language);
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
// return Err(ServerFnError::new("Not implemented"));
let mm = get_app_state().model_manager.clone();
let recipe = RecipeBmc::get_recipe_by_id(&mm, id, &language)
.await
.map_err(|e| {
tracing::error!("Error getting recipe: {:?}", e);
ServerFnError::new("Server error")
})?
.ok_or(ServerFnError::new("Recipe not found"))?;
Ok(transform_recipe(&recipe))
}
Note that the hydration error only happens on this page that makes a server call. The home page works fine, since it's not making any calls.
@ealmloff I found out the minimal code and the reason of the panic but not fix yes.
The below code is the minimal code to reproduce hydration error.
Consequently, awating in server function will cause hydration error. I would say that it comes about the server macro issues.
#![allow(non_snake_case)]
use dioxus::prelude::*;
fn main() {
dioxus::launch(App);
}
fn App() -> Element {
let data = use_server_future(move || get_server_data())?;
rsx! {
div { "panic test" }
}
}
#[server(GetServerData)]
async fn get_server_data() -> Result<String, ServerFnError> {
// this awaiting causes hydration panic
let _ = reqwest::get("https://google.com").await;
Ok("Hello from the server!".to_string())
}
The below code is a workaround code to prevent hydration error for fullstack.
Basically, we can use reqwest crate instead of using server macro on client side.
#![allow(non_snake_case)]
use dioxus::prelude::*;
use serde::{Deserialize, Serialize};
use server_fn::codec::{GetUrl, Json};
fn main() {
dioxus::launch(App);
}
fn App() -> Element {
let data = use_server_future(move || async move {
match reqwest::get("http://localhost:8080/api/data").await {
Ok(res) => res.json::<GetResponse>().await.unwrap_or_default(),
Err(_) => GetResponse::default(),
}
});
rsx! {
div { "panic test {data:?}" }
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct GetResponse {
data: String,
}
#[server(endpoint = "/data", input=GetUrl, output=Json)]
pub async fn get_server_data() -> Result<GetResponse, ServerFnError> {
// this awaiting cause hydration panic
let _ = reqwest::get("https://google.com").await;
Ok(GetResponse {
data: "test".to_string(),
})
}
@ealmloff here are some relevant snippets from my code, which hopefully are enough to build a minimal reproduction.
The two main differences I see in that code vs my attempt at a reproduction is:
- You have a server instead of using the launch entry point for the server
- The server function is called from a child component after the Link element has been rendered
However, I still cannot reproduce the issue with this code that has both of those changes:
// [dependencies]
// axum = { version = "0.7.9", optional = true }
// dioxus = { version = "0.6.0-alpha.5", features = ["fullstack", "router"] }
// dioxus-cli-config = "0.6.0-alpha.5"
// reqwest = { version = "0.12.9", features = ["blocking"] }
// tokio = { version = "1.41.1", features = ["full"], optional = true }
// [features]
// web = ["dioxus/web"]
// server = ["dioxus/server", "dep:tokio", "dep:axum"]
use dioxus::prelude::*;
#[cfg(not(feature = "server"))]
fn main() {
launch(app);
}
#[cfg(feature = "server")]
#[tokio::main]
async fn main() {
// Get the address the server should run on. If the CLI is running, the CLI proxies fullstack into the main address
// and we use the generated address the CLI gives us
let address = dioxus_cli_config::fullstack_address_or_localhost();
// Set up the axum router
let router = axum::Router::new()
// You can add a dioxus application to the router with the `serve_dioxus_application` method
// This will add a fallback route to the router that will serve your component and server functions
.serve_dioxus_application(ServeConfigBuilder::default(), app);
// Finally, we can launch the server
let router = router.into_make_service();
let listener = tokio::net::TcpListener::bind(address).await.unwrap();
axum::serve(listener, router).await.unwrap();
}
fn app() -> Element {
rsx! {
document::Link {
rel: "stylesheet",
href: asset!("/assets/main.css"),
}
ChildComponent {}
}
}
fn ChildComponent() -> Element {
let server_data = use_server_future(get_server_data)?;
let mut count = use_signal(|| 0);
rsx! {
div {
h1 { "High-Five counter: {count}" }
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
"Server said: {server_data:?}"
}
}
}
#[server(GetServerData)]
async fn get_server_data() -> Result<String, ServerFnError> {
Ok("Hello from the server!".to_string())
}
@ealmloff https://github.com/hackartists/dx-hydration-err this repository is for reproduction of hydration error with docker and the minimal code.
Only you can run it with make run
Reproduction with the git latest version of dioxus:
// [dependencies]
// axum = { version = "0.7.9", optional = true }
// dioxus = { version = "0.6.0-alpha.5", features = ["fullstack", "router"] }
// dioxus-cli-config = "0.6.0-alpha.5"
// reqwest = { version = "0.12.9", features = ["blocking"] }
// tokio = { version = "1.41.1", features = ["full"], optional = true }
// [features]
// web = ["dioxus/web"]
// server = ["dioxus/server", "dep:tokio", "dep:axum"]
use dioxus::prelude::*;
#[cfg(not(feature = "server"))]
fn main() {
launch(app);
}
#[cfg(feature = "server")]
#[tokio::main]
async fn main() {
// Get the address the server should run on. If the CLI is running, the CLI proxies fullstack into the main address
// and we use the generated address the CLI gives us
let address = dioxus_cli_config::fullstack_address_or_localhost();
// Set up the axum router
let router = axum::Router::new()
// You can add a dioxus application to the router with the `serve_dioxus_application` method
// This will add a fallback route to the router that will serve your component and server functions
.serve_dioxus_application(ServeConfigBuilder::default(), app);
// Finally, we can launch the server
let router = router.into_make_service();
let listener = tokio::net::TcpListener::bind(address).await.unwrap();
axum::serve(listener, router).await.unwrap();
}
fn app() -> Element {
// document::Link {
// rel: "stylesheet",
// href: asset!("/assets/main.css"),
// }
rsx! {
ChildComponent {}
}
}
fn ChildComponent() -> Element {
let server_data = use_server_future(get_server_data)?;
let mut count = use_signal(|| 0);
rsx! {
div {
h1 { "High-Five counter: {count}" }
button { onclick: move |_| count += 1, "Up high!" }
button { onclick: move |_| count -= 1, "Down low!" }
"Server said: {server_data:?}"
}
}
}
#[server(GetServerData)]
async fn get_server_data() -> Result<String, ServerFnError> {
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
Ok("Hello from the server!".to_string())
}
Seem to be hitting this on 0.6.0-rc.0, any workarounds?..
Seem to be hitting this on
0.6.0-rc.0, any workarounds?..
Refer to the above conversation to see workarounds https://github.com/DioxusLabs/dioxus/issues/3041#issuecomment-2511699509
There are two worksarounds;
- Make a call with
reqwestwithuse_server_futureinstead of depending onservermacros - Temporariliy use
use_resourceinstead ofuse_server_future
Basically, I believe it will be fixed, closed next version. @valyagolev