leptos-rs / leptos

Build fast web applications with Rust.
https://leptos.dev
MIT License
15.31k stars 599 forks source link

Nested Suspense ignores SSR Mode and breaks Hydration #2561

Open luxalpa opened 2 months ago

luxalpa commented 2 months ago

Describe the bug When using a Suspense within another Suspense with PartiallyBlocked SSR mode and two blocking resources, it will only block for the outer one, but not wait for the inner one. Furthermore, a component (possibly others) that renders after the Suspense won't hydrate properly and instead create <DynChild> and <> comment nodes.

Leptos Dependencies

Please copy and paste the Leptos dependencies and features from your Cargo.toml.

For example:

leptos = { version = "0.6.11", features = ["nightly"] }
leptos_meta = { version = "0.6.11", features = ["nightly"] }
leptos_actix = { version = "0.6.11", optional = true }
leptos_router = { version = "0.6.11", features = ["nightly"] }
leptos_dom = "0.6.11"
leptos_reactive = { version = "0.6.11" }
leptos_macro = { version = "0.6.11" }
serde = "1.0.198"

To Reproduce

use leptos::*;
use leptos_meta::*;
use leptos_router::{Route, Router, Routes, SsrMode};
use std::time::Duration;

#[component]
pub fn App() -> impl IntoView {
    provide_meta_context();

    view! {
        <Router>
            <div>
                "Hello World"
                    <Routes>
                        <Route path="/" view=|| {
                            view! {<FrontPage />}
                        } ssr=SsrMode::PartiallyBlocked />
                    </Routes>
            </div>
            <For each=move || vec![1, 2, 3] key=|k| *k children=|i| {
                view! {
                    <div>
                        {i}
                    </div>
                }
            } />
        </Router>
    }
}

#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct UserData {
    name: String,
}

#[derive(serde::Serialize, serde::Deserialize, Debug, Clone)]
pub struct SiteData {
    contents: String,
}

#[server]
pub async fn get_user_data() -> Result<UserData, ServerFnError> {
    return Ok(UserData {
        name: "Luxalpa".to_string(),
    });
}

#[server]
pub async fn get_site_data() -> Result<SiteData, ServerFnError> {
    use actix_web::HttpRequest;
    use leptos_actix::extract;

    let req: HttpRequest = extract().await.unwrap();
    println!("Method: {:?}", req.method());

    return Ok(SiteData {
        contents: "Welcome to Leptos".to_string(),
    });
}

#[component]
pub fn FrontPage() -> impl IntoView {
    let res = create_blocking_resource(|| (), |_| async move { get_user_data().await.unwrap() });

    let contents = move || {
        res.with(|data| {
            let Some(data) = data.as_ref() else {
                return ().into_view();
            };

            let res2 =
                create_blocking_resource(|| (), |_| async move { get_site_data().await.unwrap() });

            // spawn_local(async move {
            //     let site_data = get_site_data().await.unwrap();  // <- this code is also broken, but I'll probably file a separate issue
            //     println!("Site Data: {:?}", site_data);
            // });

            let name = data.name.clone();

            (view! {
                <div>
                    <Suspense>
                        {move || res2.get().map(|data| data.contents)}
                    </Suspense>
                    {name}
                </div>
            })
            .into_view()
        })
    };

    view! {
        <div>
            <Suspense>
                {contents}
            </Suspense>
        </div>
    }
}

Expected behavior Render the entire HTML first instead of rendering it later. Properly hydrate the created components

Additional context My outer suspense / resource is loading UserData depending on whether the user is logged in or not. Then the inner suspense / resource is loading some application data that is only allowed to be loaded if the user is logged in. Unfortunately it seems that I cannot avoid nesting Suspense's here, as I can't load all possible userdata on the outer context, and if I load only the data necessary for the current page, then it won't work if the user starts on a different page but then navigates to this one. I have a large app and several pages have this issue, so any hacky workaround will also involve tons of work.

gbj commented 2 months ago

Hm. This one does seem to be fixed by https://github.com/leptos-rs/leptos/pull/2284, which I closed because I thought it broke other things. It may be worth testing your full example against that branch to see if it does break other things. I can revisit it.

The current Suspense system is very fragile and is one of the things that is definitely being improved in 0.7 wheni it comes. Sorry for the pain for now.

luxalpa commented 2 months ago

Yes, the change does resolve this issue. I'll see if it breaks any further things but it does look pretty good right now. Definitely excited for 0.7 but also a bit scared of all the things that it might be breaking :X