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

Wait for Suspense to Finish Before Removing Elements #1335

Open
ealmloff opened this issue Aug 10, 2023 · 1 comment · May be fixed by #2365
Open

Wait for Suspense to Finish Before Removing Elements #1335

ealmloff opened this issue Aug 10, 2023 · 1 comment · May be fixed by #2365
Assignees
Labels
core relating to the core implementation of the virtualdom enhancement New feature or request

Comments

@ealmloff
Copy link
Member

Specific Demand

When a component becomes suspended, we should hold the old elements while the suspense is running while the suspense is running.

fn Suspends(cx: Scope) -> Element {
    let running = use_state(cx, || false);
    let count = use_state(cx, || 0);
    
    if running {
        to_owned![running, count];
        cx.spawn(async {
            // wait one second ...
            running.set(true);
            count.set(123);
        });
        return cx.suspend();
    }
    
    render! {
        button {
            onclick: move |_| running.set(true),
            "Suspend"
        }
        "{count}"
    }
}

Implement Suggestion

Instead of rendering nothing like the current suspense implementation does, we should render the button as normal until suspense if finished

@ealmloff ealmloff added enhancement New feature or request core relating to the core implementation of the virtualdom labels Aug 10, 2023
@ealmloff ealmloff added this to the 0.5 Release milestone Aug 10, 2023
@ealmloff ealmloff self-assigned this Dec 18, 2023
@ealmloff
Copy link
Member Author

ealmloff commented Apr 15, 2024

The suspense API we currently have is functional, but it doesn't support choosing a placeholder to render while a component is suspended. Here is the same component with a few different suspense APIs

  1. Baseline, no suspense

This doesn't support waiting for a future to render on the server which makes proper SSR difficult

#[component]
fn Doggo() -> Element {
    let mut fut = use_resource(move || async move { async_function().await });

    match fut.read_unchecked().as_ref() {
        Some(Ok(resp)) => rsx! {
            button { onclick: move |_| fut.restart(), "Click to fetch another doggo" }
            div { img { max_width: "500px", max_height: "500px", src: "{resp.message}" } }
        },
        Some(Err(_)) => rsx! { div { "loading dogs failed" } },
        None => rsx! { div { "loading dogs..." } },
    }
}
  1. A suspense component with error handling

This is fairly simple because it uses the existing component API, but it requires nesting for each suspended future

#[component]
fn Doggo() -> Element {
    rsx! {
        Suspense {
            future: move || async { async_function().await },
            finished: move |resp, suspense| {
                rsx! {
                    button { onclick: move |_| suspense.restart(), "Click to fetch another doggo" }
                    div { img { max_width: "500px", max_height: "500px", src: "{resp.message}" } }
                }
            },
            loading: rsx! { div { "loading dogs..." } },
            error: |err| rsx! { div { "loading dogs failed {err}" } },
        }
    }
}
  1. A suspense extension trait similar to the throw trait

Unlike 2 this doesn't require as much nesting, but it introduces early returns which can make hooks more difficult to reason about

// Suspense extension with throw trait
#[component]
fn Doggo() -> Element {
    let mut fut = use_resource(move || async move { async_function().await })
        .suspend()
        .placeholder(|| rsx! { div { "loading dogs..." } })?
        .throw()
        .context(|err| {
            rsx! {
                "loading dogs failed ({err})"
            }
        })?;

    let resp = fut();

    rsx! {
        button { onclick: move |_| fut.restart(), "Click to fetch another doggo" }
        div { img { max_width: "500px", max_height: "500px", src: "{resp.message}" } }
    }
}
  1. Context based suspense (may be combined with the current .suspend() API or the improved version in 3)

This allows you to handle suspense at a single boundary which makes it easier to show an unified loading UI. It also uses early returns and components

// Suspense context
#[component]
fn Parent() -> Element {
    rsx! {
        ErrorBoundary {
            handle_error: |err| rsx! { div { "Error: {err}" } },
            Suspense {
                pending: rsx! { div { "loading..." } },
                Doggo {}
            }
        }
    }
}

#[component]
fn Doggo() -> Element {
    let mut fut = use_resource(move || async move { async_function().await })
        .suspend()?
        .throw()?;

    let resp = fut();

    rsx! {
        button { onclick: move |_| fut.restart(), "Click to fetch another doggo" }
        div { img { max_width: "500px", max_height: "500px", src: "{resp.message}" } }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
core relating to the core implementation of the virtualdom enhancement New feature or request
Projects
Status: No status
1 participant