leptos-rs / leptos

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

Context provided inside suspense doesn't reach `Outlet` #3042

Open zakstucke opened 1 month ago

zakstucke commented 1 month ago

Describe the bug Trying to implement recommended approach we were discussing, providing context inside suspend doesn't seem to work.

Tested in the counter_isomorphic example.

Leptos Dependencies Git

To Reproduce

#[derive(Debug, Default, Clone)]
struct CtxTopLevel;

#[derive(Debug, Default, Clone)]
struct CtxOutsideSuspense;

#[derive(Debug, Default, Clone)]
struct CtxInsideSuspense;

#[component]
pub fn Counters() -> impl IntoView {
    provide_context(CtxTopLevel);
    view! {
        <main>
            <Router>
                <Routes fallback={|| {}}>
                    <ParentRoute
                        path={path!("/")}
                        view={AuthProvider}
                    >
                        <Route
                            path={path!("/")}
                            view={|| {
                                expect_context::<CtxTopLevel>();
                                expect_context::<CtxOutsideSuspense>();
                                expect_context::<CtxInsideSuspense>();
                                view! { <p>HOME</p> }
                            }}
                        />
                    </ParentRoute>
                </Routes>
            </Router>
        </main>
    }
}

#[component]
pub fn AuthProvider() -> impl IntoView {
    let r_auth_state = Resource::new_blocking(
        || {},
        |_| {
            // AuthManager::refresh_token()
            async { "user_details...".to_string() }
        },
    );

    provide_context(CtxOutsideSuspense);

    let inner_view = move || Suspend::new(async move {
        let _ = r_auth_state.await;
        provide_context(CtxInsideSuspense);
        view! { <Outlet /> }
    });
    view! { <Suspense>{inner_view}</Suspense> }
}
listening on http://127.0.0.1:3000
thread 'actix-rt|system:0|arbiter:1' panicked at /Users/zak/z/code/leptos/reactive_graph/src/owner/context.rs:265:9:
Location { file: "src/counters.rs", line: 54, col: 33 } expected context of type "counter_isomorphic::counters::CtxInsideSuspense" to be present
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
gbj commented 1 month ago

The example I gave in the other issue (#3038) does work, though, which is odd. Maybe worth investigating what the difference is between those two cases?

#[component]
pub fn App() -> impl IntoView {
    view! {
        <Router>
            <Routes fallback=|| {
                view! { <p>404</p> }
            }>
                <ParentRoute path=path!("/") view=AuthCheck>

                    <Route path=path!("/") view=Home/>
                    <Route path=path!("/other") view=Other/>
                </ParentRoute>
            </Routes>
        </Router>
    }
}

#[component]
fn AuthCheck() -> impl IntoView {
    let resource = Resource::new_blocking(
        || (),
        move |_| async move {
            #[cfg(feature = "hydrate")]
            send_wrapper::SendWrapper::new(
                gloo_timers::future::TimeoutFuture::new(1000),
            )
            .await;
            #[cfg(feature = "ssr")]
            tokio::time::sleep(tokio::time::Duration::from_millis(1000)).await;

            Some("user_info...".to_string())
        },
    );

    let inner_view = Suspend::new(async move {
        let user_info = resource.await;
        provide_context(user_info.clone());
        (
            if user_info.is_some() {
                Either::Left(view! {
                    <div>
                        <p>"Logged in!"</p>
                    </div>
                })
            } else {
                Either::Right(view! { <p>"Logged out!"</p> })
            },
            Outlet(),
        )
    });
    view! { <Suspense>{inner_view}</Suspense> }
}

#[component]
fn Home() -> impl IntoView {
    let user_info = expect_context::<Option<String>>();
    format!("{user_info:?}")
}

#[component]
fn Other() -> impl IntoView {
    "Other Page"
}
zakstucke commented 1 month ago

Hey @gbj , the difference is I have move || Suspend(...) in this repro, in yours you forgot to make Suspend reactive (it's reactive in any other 0.7 example i've seen), which works in the limited repro you gave, but breaks in a bunch of other ways.

If nonreactive Suspend is actually meant to work, I can open a separate issue as to how it doesn't work.

But yeah if you change Suspend -> move || Suspend in your repro from the last issue, you hit this error too.

gbj commented 1 month ago

Ah okay, I see.

So, here's the problem:

Currently, an Outlet's view is slotted into the ownership tree as the child of the parent route's view, not as the child of the location where it's used. So the move || and the Outlet are actually siblings in the ownership graph. (This may be confusing; I'm happy to expand on it if it's helpful.)

A workaround is pretty straightforward, and consists of providing the context in the parent Owner:

    let outer_owner = Owner::current().unwrap();

    let inner_view = move || {
        Suspend::new({
            let outer_owner = outer_owner.clone();
            async move {
                let auth_state = r_auth_state.await;
                provide_context(CtxInsideSuspense);
                Outlet()
            }
        })
    };
    view! { <Suspense>{inner_view}</Suspense> }

I think it is possible to fix this, but setting the ownership/context relationships between nested routes is relatively tricky so it will take a bit more time than I have at the moment.

zakstucke commented 1 month ago

Got it, the workaround seems to work for now thank you. It might be worth editing your workaround snippet with outer_owner.with(|| provide_context(...)) which I think you missed, to help anyone else reading this before it's fixed.

so it will take a bit more time than I have at the moment

Totally understand, thanks for putting up with my tsunami of issues the last few days! At the very least I hope I helped push towards a stable 0.7.