dioxus icon indicating copy to clipboard operation
dioxus copied to clipboard

Wait for Suspense to Finish Before Removing Elements

Open ealmloff opened this issue 2 years ago • 1 comments

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 avatar Aug 10 '23 00:08 ealmloff

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}" } }
    }
}

ealmloff avatar Apr 15 '24 19:04 ealmloff