leptos-rs / leptos

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

[0.7] runtime error: stored children already disposed #3012

Closed winteler closed 1 month ago

winteler commented 1 month ago

Hello,

I think I've found a bug with stored children. It occurs when the result of a resource and a params are used together. I made a small example below to reproduce (not exactly like my app but I imagine it's the same error).

Leptos Dependencies

leptos = { version = "0.7.0-beta5", features = ["nightly"] }
leptos_axum = { version = "0.7.0-beta5", optional = true }
leptos_meta = { version = "0.7.0-beta5" }
leptos_router = { version = "0.7.0-beta5", features = ["nightly"] }

I also tried with rev = "17821f863a543feb70ccc70e0426deb78b6419ef" to see if https://github.com/leptos-rs/leptos/commit/17821f863a543feb70ccc70e0426deb78b6419ef made a difference but the error still occurred.

To Reproduce

use leptos::either::Either;
use leptos::prelude::*;
use leptos_meta::{provide_meta_context, MetaTags, Stylesheet, Title};
use leptos_router::{components::{Route, Router, Routes}, ParamSegment, StaticSegment};
use leptos_router::components::{Outlet, ParentRoute};
use leptos_router::hooks::use_params_map;

pub fn shell(options: LeptosOptions) -> impl IntoView {
    view! {
        <!DOCTYPE html>
        <html lang="en">
            <head>
                <meta charset="utf-8"/>
                <meta name="viewport" content="width=device-width, initial-scale=1"/>
                <AutoReload options=options.clone() />
                <HydrationScripts options/>
                <MetaTags/>
            </head>
            <body>
                <App/>
            </body>
        </html>
    }
}

#[server]
pub async fn data() -> Result<i32, ServerFnError> {
    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
    Ok(0)
}

#[server]
pub async fn nested_data() -> Result<String, ServerFnError> {
    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
    Ok(String::from("nested data result"))
}

#[component]
pub fn App() -> impl IntoView {
    // Provides context that manages stylesheets, titles, meta tags, etc.
    provide_meta_context();

    view! {
        // injects a stylesheet into the document <head>
        // id=leptos means cargo-leptos will hot-reload this stylesheet
        <Stylesheet id="leptos" href="/pkg/reproduce-leptos-0-7-beta5.css"/>

        // sets the document title
        <Title text="Welcome to Leptos"/>

        // content for this welcome page
        <Router>
            <main>
                <Routes fallback=|| "Page not found.".into_view()>
                    <Route path=StaticSegment("") view=HomePage/>
                    <ParentRoute path=ParamSegment("param") view=ParamPage>
                        <Route path=StaticSegment("") view=BasePage/>
                        <Route path=StaticSegment("/other") view=TestPage/>
                    </ParentRoute>
                </Routes>
            </main>
        </Router>
    }
}

/// Renders the home page of your application.
#[component]
fn HomePage() -> impl IntoView {
    view! {
        <h1>"Welcome"</h1>
        <a href="/0/other">"Click here to go to the test page"</a>
    }
}

/// Renders the home page of your application.
#[component]
fn ParamPage() -> impl IntoView {
    view! {
        <h1>"Param page"</h1>
        <Outlet/>
    }
}

/// Renders the home page of your application.
#[component]
fn BasePage() -> impl IntoView {
    view! {
        <h2>"Base page"</h2>
        <a href="/0/other">"TestPage, param = 0"</a>
    }
}

/// The error occurs when navigating from Test Page to Base page with a different param.
#[component]
fn TestPage() -> impl IntoView {
    let param_map = use_params_map();
    let param_value = move || param_map.read().get_str("param").unwrap_or_default().to_string();
    let resource = Resource::new(
        move || param_value(),
        move |_| {
            log!("Load data.");
            data()
        }
    );
    view! {
        <h2>"Test page"</h2>
        <a href="/1">"Click here to navigate to BasePage with param = 1, leads to runtime error."</a>
        <SuspenseUnpack resource=resource let:value>
        {
            match param_value().parse::<i32>() {
                Ok(param_value) if param_value == *value => view! {
                    <div>{"Param is equal to resource."}</div>
                }.into_any(),
                _ => view! { <div>"Param is not equal to resource."</div> }.into_any(),
            }
        }

        </SuspenseUnpack>
    }
}

#[component]
pub fn SuspenseUnpack<
    T: Clone + Send + Sync + 'static,
    V: IntoView + 'static,
    F: Fn(&T) -> V + Clone + Send + Sync + 'static,
>(
    resource: Resource<Result<T, ServerFnError>>,
    children: F,
) -> impl IntoView {
    let children = StoredValue::new(children);

    view! {
        <Suspense>
        {
            move || Suspend::new(async move {
                match &resource.await {
                    Ok(value) => Either::Left(children.get_value()(value)),
                    Err(_e) => Either::Right(view! { <div>"Error"</div> }),
                }
            })
        }
        </Suspense>
    }
}

Additional context You might need to go from <BasePage/> to <TestPage/> more than once to get the error. In this particular example, the error doesn't seem to be such a big deal as you can continue to navigate but in my app it leads to other errors (that disappear if I read from the param_map with _untracked).

In that case, a part of the issue is that when navigating to <BasePage/>, the resource is reloaded despite it no longer being rendered on the new page. Would there be a relatively simple way to prevent the resource from reloading? That would be nice to avoid unnecessary requests.

Please let me know if you need any other information and thanks for your help!

winteler commented 1 month ago

I also wanted to mention that I got a similar error (not sure if it's exactly the same or not) with the code below that is quite a bit simpler. The error occurs when navigating to <TestPage/> and then back to <HomePage/> before the resource has finished loading.

use leptos::either::Either;
use leptos::prelude::*;
use leptos_meta::{provide_meta_context, MetaTags, Stylesheet, Title};
use leptos_router::{components::{Route, Router, Routes}, StaticSegment};

pub fn shell(options: LeptosOptions) -> impl IntoView {
    view! {
        <!DOCTYPE html>
        <html lang="en">
            <head>
                <meta charset="utf-8"/>
                <meta name="viewport" content="width=device-width, initial-scale=1"/>
                <AutoReload options=options.clone() />
                <HydrationScripts options/>
                <MetaTags/>
            </head>
            <body>
                <App/>
            </body>
        </html>
    }
}

#[server]
pub async fn data() -> Result<String, ServerFnError> {
    tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;
    Ok(String::from("hello world"))
}

#[component]
pub fn App() -> impl IntoView {
    // Provides context that manages stylesheets, titles, meta tags, etc.
    provide_meta_context();

    view! {
        // injects a stylesheet into the document <head>
        // id=leptos means cargo-leptos will hot-reload this stylesheet
        <Stylesheet id="leptos" href="/pkg/reproduce-leptos-0-7-beta5.css"/>

        // sets the document title
        <Title text="Welcome to Leptos"/>

        // content for this welcome page
        <Router>
            <main>
                <Routes fallback=|| "Page not found.".into_view()>
                    <Route path=StaticSegment("/") view=HomePage/>
                    <Route path=StaticSegment("/test") view=TestPage/>
                </Routes>
            </main>
        </Router>
    }
}

/// Renders the home page of your application.
#[component]
fn HomePage() -> impl IntoView {
    view! {
        <h1>"This is just a homepage for redirection"</h1>
        <a href="/test">"Test page"</a>
    }
}

/// The error occurs when navigating from Test Page to Base page before the resource finishes loading.
#[component]
fn TestPage() -> impl IntoView {
    let resource = Resource::new(
        move || (),
        move |_| {
            data()
        }
    );
    view! {
        <h2>"Test page"</h2>
        <div>"The runtime errors occurs when navigating to the home page before the resource finishes loading"</div>
        <a href="/">"BasePage"</a>
        <SuspenseUnpack resource=resource let:value>
            <div>{value.clone()}</div>
        </SuspenseUnpack>
    }
}

#[component]
pub fn SuspenseUnpack<
    T: Clone + Send + Sync + 'static,
    V: IntoView + 'static,
    F: Fn(&T) -> V + Clone + Send + Sync + 'static,
>(
    resource: Resource<Result<T, ServerFnError>>,
    children: F,
) -> impl IntoView {
    let children = StoredValue::new(children);

    view! {
        <Suspense>
        {
            move || Suspend::new(async move {
                match &resource.await {
                    Ok(value) => Either::Left(children.with_value(|children| children(value))),
                    Err(_e) => Either::Right(view! { <div>"Error"</div> }),
                }
            })
        }
        </Suspense>
    }
}

Thanks again for your work :)