dioxus icon indicating copy to clipboard operation
dioxus copied to clipboard

`use_server_future` Hydration error

Open hackartists opened this issue 1 year ago • 13 comments

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_resource works properly.
    • use_server_future causes 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

image

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

hackartists avatar Oct 08 '24 11:10 hackartists

#[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)

ealmloff avatar Oct 08 '24 14:10 ealmloff

#[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.

hackartists avatar Oct 09 '24 11:10 hackartists

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())
}

ealmloff avatar Oct 09 '24 13:10 ealmloff

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.

ochrons avatar Nov 27 '24 09:11 ochrons

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.

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 avatar Nov 27 '24 14:11 ealmloff

@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.

hackartists avatar Nov 27 '24 21:11 hackartists

@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.

ochrons avatar Nov 28 '24 08:11 ochrons

@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(),
    })
}

hackartists avatar Dec 02 '24 14:12 hackartists

@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:

  1. You have a server instead of using the launch entry point for the server
  2. 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 avatar Dec 02 '24 18:12 ealmloff

@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

hackartists avatar Dec 02 '24 21:12 hackartists

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())
}

ealmloff avatar Dec 03 '24 17:12 ealmloff

Seem to be hitting this on 0.6.0-rc.0, any workarounds?..

valyagolev avatar Dec 04 '24 11:12 valyagolev

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 reqwest with use_server_future instead of depending on server macros
  • Temporariliy use use_resource instead of use_server_future

Basically, I believe it will be fixed, closed next version. @valyagolev

hackartists avatar Dec 04 '24 21:12 hackartists