Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hydration bug wrt resource-dependent content #3398

Open
Jazzpirate opened this issue Dec 21, 2024 · 6 comments
Open

Hydration bug wrt resource-dependent content #3398

Jazzpirate opened this issue Dec 21, 2024 · 6 comments

Comments

@Jazzpirate
Copy link

Describe the bug
If content depends on the result of at least two resources in somewhat non-direct ways, it is possible to get client errors "A hydration error occurred while trying to hydrate an element", where the client seemingly expects the HTML that would be there if both resources have been resolved, but the server sends the fallback for resource 2, with the resolution attached in a <script>-node at the end. This seems to be new to v0.7.1.

Two cases I have found where this occurs:

  1. Resource r1 sets signal s, signal s leads to a path that loads resource r2, later content is conditional on the result of r2.
  2. More direct: Resource r1 is provided as context, a later component branches on r1 and loads r2, later content is conditional on the results of both r1 and r2.

Leptos Dependencies

Exactly those of LEPTOS_CARGO_GENERATE_VERSION=v0.22.0 cargo leptos new --git https://github.com/leptos-rs/start-axum

To Reproduce
replace the App and HomePage components of the start-axum template by the following:


#[server]
async fn simple() -> Result<bool,ServerFnError> {
    Ok(true)
}

#[server]
async fn more_complex() -> Result<Vec<String>,ServerFnError> {
    Ok((1..20).map(|i| i.to_string()).collect())
}

#[component]
pub fn App() -> impl IntoView {
    provide_meta_context();
    view! {
        <Stylesheet id="leptos" href="/pkg/test.css"/>
        <Title text="Welcome to Leptos"/>
        <Router>
            <main>
                <Routes fallback=|| "Page not found.".into_view()>
                    <Route ssr=leptos_router::SsrMode::PartiallyBlocked path=StaticSegment("") view=HomePage/>
                </Routes>
            </main>
        </Router>
    }
}

#[component]
fn HomePage() -> impl IntoView {
    let r1 = Resource::new_blocking(|| (),|()| simple());
    provide_context(r1);
    view! {
        <Suspense fallback=|| view!(<div>"Loading..."</div>)>{ move || {
            match r1.get() {
                Some(Ok(true)) => "true".to_string(),
                Some(Ok(false)) => "false".to_string(),
                None => "This shouldn't happen".to_string(),
                Some(Err(e)) => e.to_string()
            }
        }}</Suspense>
        <Complex/>
    }
}

#[component]
fn Complex() -> impl IntoView {
    use leptos::either::EitherOf3;
    let r2 = Resource::new_blocking(|| (), |()| more_complex());
    let o = expect_context::<Resource<Result<bool,ServerFnError>>>();
    view!(<Suspense>{
        move || o.get().map(|b| view!{
            {b.unwrap().to_string()}
            <Suspense fallback=|| view!(<div>"Loading..."</div>)>{move || {
                match r2.get() {
                    Some(Ok(v)) => EitherOf3::A(view!(<ul>
                        {v.into_iter().map(|s| view!(<li>{s}</li>) ).collect_view()}
                    </ul>)),
                    Some(Err(e)) => EitherOf3::B(view!(<div>{e.to_string()}</div>)),
                    None => EitherOf3::C(view!(<div>Loading...</div>))
                }
            }}</Suspense>
        })
    }</Suspense>)
}

Expected behavior
that the client doesn't panic. More precisely: Either that both resources are fully resolved server-side and the resulting HTML is fully resolved, or that the results of the resources are embedded in script nodes in the end and that is what the client expects, or that the HTML contains the fallbacks and the client knows to request the second resource from the server. Probably depending on the Ssr-mode (I'm definitely out of my depth here).

Screenshots
Screenshot from 2024-12-21 22-44-57

Additional context
Add any other context about the problem here.

@Jazzpirate
Copy link
Author

Addendum: The HTML sent by the server looks like this:

<main><!--hot-reload|src-app.rs-56|open-->true<!--hot-reload|src-app.rs-74|open-->true<!--s-2-1-o--><!--hot-reload|src-app.rs-77|open-->
<div>Loading...</div>
<!--hot-reload|src-app.rs-77|close--><!--s-2-1-c--><!--hot-reload|src-app.rs-74|close--><!--hot-reload|src-app.rs-56|close--></main><!--hot-reload|src-app.rs-39|close--></body></html><!--hot-reload|src-app.rs-9|close-->
<template id="2-1-f"><ul><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>10</li><li>11</li><li>12</li><li>13</li><li>14</li><li>15</li><li>16</li><li>17</li><li>18</li><li>19</li><!></ul></template>

^ Note that even though all the <li> nodes are there (the result of r2), there is also still the <div>Loading...</div> from the fallback (or the None branch of the pattern match?)

@gbj
Copy link
Collaborator

gbj commented Dec 21, 2024

Just for the sake of clarity, this is not in fact a regression in 0.7.1. If you set the version to 0.7.0, the same error still occurs:

leptos = { version = "=0.7.0" }

Note also that it requires the two true nodes to be directly-adjacent text nodes in the DOM. i.e., if either the parent Suspense in HomePage or the {b.unwrap().to_string()} is wrapped by an element (say, a <p>), there's no hydration error.

Finally, note that the behavior is the same regardless of SSR mode -- i.e., it breaks on SsrMode::Async just as it does on out-of-order streaming.

I suppose this is because <Suspense/>, during HTML rendering, sets the position of the next node to NextChild, when — if it's after a plain text node — it needs to be NextChildAfterText to make sure the two text nodes are kept separate by an additional marker comment.

Am I correct in the hypothesis that, in your actual/non-minimal use case, this happens with two adjacent text nodes in two separate <Suspense/> components? Or is that just the explanation in this very specific case?

I really do think it's about the adjacent text nodes, though. Here's a much-less-complex Complex that shows the same behavior -- it's triggered by the <span/> (i.e., the first node that follows after the second of the two adjacent text nodes)

#[component]
fn Complex() -> impl IntoView {
    let r2 = Resource::new_blocking(|| (), |()| more_complex());

    view!(<Suspense>{
        move || r2.get().map(|b| view!{
            "hello!"
            <span/>
        })
    }</Suspense>)
}

@Jazzpirate
Copy link
Author

Am I correct in the hypothesis that, in your actual/non-minimal use case, this happens with two adjacent text nodes in two separate components? Or is that just the explanation in this very specific case?

Unfortunately not :( If I insert <span>"foo"</span> in front of all <Suspense> nodes, the error does go away in the minimal example, but not in my actual project. But the "symptom" is the same as in the example in that the HTML contains the fallback (a thaw::Spinner, in the actual use case) even though the resource has been resolved server-side already, so I thought it was the same problem. One difference though is that the result of r2 ends up in a template in the example here, but only in a script node in the actual project.

I'll do some more investigating and try to get another non-working minimal example going. Thanks for your help so far

@Jazzpirate
Copy link
Author

Update: It was not leptos 0.7.1; thaw also got an update, and reverting that from 0.4-rc to 0.4-beta5 makes it go away. It's not entirely clear to me where thaw components are involved at that point, but that probably means the actual problem is much earlier in the DOM. (The spinner I use is actually a copy-paste with modified CSS of the thaw one, so that didn't change).

It is of course possible that the problem with thaw 0.4-rc is related to the issue above with consecutive text nodes, but that will take a lot more digging to figure out.

Either way, thanks for your efforts; I will leave this issue open in case you want it as a reminder for the consecutive-text-nodes-problem; otherwise feel free to close.

@spencewenski
Copy link
Contributor

I’m running into something similar (but I’m not using Thaw, just plain Leptos + tailwind). I’m trying to figure out a minimal reproduction.

@spencewenski
Copy link
Contributor

I think my issue is actually slightly different -- I can only reproduce it when using islands. I opened a separate issue with a minimal repro: #3419

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants