yew icon indicating copy to clipboard operation
yew copied to clipboard

Suspension busy polling instead of waiting

Open tdooms opened this issue 3 years ago • 4 comments

Problem When trying out the new suspension feature I noticed the component is just trying to be rendered in a loop without any regard for the HtmlResult.

use yew::prelude::*;
use yew::suspense::{Suspension, SuspensionResult};

#[hook]
fn use_name() -> SuspensionResult<String> {
    let (s, _handle) = Suspension::new();
    Err(s)
}

#[function_component(Content)]
fn content() -> HtmlResult {
    log::info!("rendering page");
    let name = use_name()?;
    Ok(html! {<div>{"Hello, "}{name}</div>})
}

#[function_component(Test)]
fn test() -> Html {
    let fallback = html! {<div>{"Loading..."}</div>};
    html! {<Suspense {fallback}> <Content /> </Suspense>}
}

pub fn main() {
    wasm_logger::init(wasm_logger::Config::default());
    yew::Renderer::<Test>::new().render();
}

This just results continuous running of the content() function, no fallback is shown.

Steps To Reproduce Steps to reproduce the behavior:

  1. New cargo project
  2. Set up the a trunk project as described in the docs with the aforementioned code
  3. Add dependencies to cargo (log, wasm-logger, yew)
  4. trunk serve --open

Expected behavior Show the fallback ("Loading...") indefinitely

Screenshots error

Environment:

  • Yew version: 'master'
  • Rust version:1.59
  • Build tool, if relevant: trunk
  • OS, if relevant: linux
  • Browser and version, if relevant: firefox 98.0.2

Questionnaire

  • [ ] I'm interested in fixing this myself but don't know where to start
  • [ ] I would like to fix and I have a solution
  • [ ] I don't have time to fix this right now, but maybe later

tdooms avatar Apr 03 '22 08:04 tdooms

You should hold the SuspensionHandle until your asynchronous task finishes.

When the SuspensionHandle is dropped, the suspension is considered to be resumed and would result in a re-render. Upon re-render, a new suspension is created, so the component will be marked as suspended again. This results in a dead loop.

In this case, you can try to hold your SuspensionHandle in a use_state hook and it should prevent it from creating a loop.

futursolo avatar Apr 03 '22 08:04 futursolo

This should be mentioned in the documentation. There's no mention of drop behavior on https://yew.rs/docs/next/concepts/suspense

ranile avatar Apr 03 '22 09:04 ranile

Thanks for the help!

I know that the feature is still very recent but I feel like the documentation is missing a few other things. I'm still quite new so maybe I'm missing some things or lack some understanding.

To explain I will write down my experience and though process for what I want to program: I'm trying to use a suspense to query some data (only) on first render. While this seems quite general, I'm struggling to write this without too much boilerplate.

To query the data I have an async function. In the examples I found that I can use Suspense::from_future for this when enabling the "tokio" flag. This also isn't mentioned in the docs.

To achieve the query only on first render I would normally use use_effect_with_deps but I can't use the Suspense::from_future there because then I can't return the suspense.

I can use Suspense::new move the handle into an use_effect_with_deps which moves it into a spawn_local that executes the future and resume manually. But the problem there is that, as there is no indication whether use_effect_.. ran this cycle, I can't easily check if I need to return the suspension or not. I could check if the result is loaded in my state which is also annoying as the loaded result could also be the default one. Using an option would leak into other parts of my code which I don't want. So, at the moment I'm using a bool to indicate whether it's the first render. The whole thing is quite verbose.

So yeah I feel like I'm missing something.

Also use_state doesn't play nice with the SuspensionHandle as the resume() is consuming and I can't (afaik) move or take out of the use_state so I can't really use that without interior mutability stuff.

tdooms avatar Apr 03 '22 13:04 tdooms

The suspension-based API is primarily designed for an http client (or other asynchronous task) to design a render-as-you-fetch pattern where the cached data is held by the client somewhere else and the asynchronous task is spawned during rendering process if more data needs to be fetched.

It can be made to work with a fetch-on-render (fetch in use_effect) pattern as well.

#[derive(Clone)]
struct Data;

#[function_component]
fn Comp() -> HtmlResult {

    let data_handle = use_state(
        || {
            let (s, handle) = Suspension::new();
            (Err(s), Some(handle))
        }
    );

    {
        let data_handle = data_handle.clone();
        use_state_with_deps(move |_| {
            spawn_local(async {
                sleep(Duration::from_secs(1)).await;
                data_handle.set((Ok(Data), None));
            });
            || {}
        }, ());
    }

    let data = data_handle.0.clone()?;

    // ... render fetched data
}

Or:

/// A component that its suspension never resumes.
#[function_component]
fn SuspendForever() -> HtmlResult {
    let (s, handle) = Suspension::new();
    use_state(move || handle);
    Err(s)
}

#[function_component]
fn Comp() -> Html {
    let data = use_state(|| -> Option<Data> { None });
    {
        let data = data.clone();
        use_effect_with_deps(|_| {
            spawn_local(async {
                sleep(Duration::from_secs(1)).await;
                data.set(Some(Data));
            });
            || {}
        }, ());
    }

    match data {
        Some(m) => { /* renders data */ }
        // we can use the suspend forever component to make suspense render its fallback,
        // it will be resumed upon unmount.
        None => html! {< SuspendForever />}
    }
}

futursolo avatar Apr 03 '22 16:04 futursolo

use_state_with_deps is typo of use_effect_with_deps? i try to use first sample code from @futursolo with use_effect_with_deps. But use_effect_with_deps is not run.

my code:

#[function_component(Comp)]
fn comp() -> HtmlResult {

    let data_handle = use_state(
        || {
            let (s, handle) = Suspension::new();
            (Err(s), Some(handle))
        }
    );

    {
        log::info!("A");
        let data_handle = data_handle.clone();
        use_effect_with_deps(move |_| {
            log::info!("B");
            spawn_local(async move {
                sleep(Duration::from_secs(1)).await;
                data_handle.set((Ok("OK!!".to_string()), None));
            });
            || {}
        }, ());
    }

    log::info!("C");
    let data: String = data_handle.0.clone()?;
    log::info!("D");
    Ok(html!{<p>{data}</p>})
}

#[function_component(Index)]
pub fn app() -> Html {
    html! {
        <Suspense fallback={html!{<div>{"loading"}</div>}}>
            <Comp />
        </Suspense>
    }
}

In console will be written following result. So, this result indicates that use_effect_with_deps is not running.

image

Shown HTML is correct because the value yet Err(suspension).

image

koji-k avatar Jan 26 '23 12:01 koji-k