Yew IntervalStream repeating problem

hi I am new to Yew and relatively new to Rust as well. I am creating a small component that polls an Api and displays the running status. It is supposed to call the endpoint every 5 seconds and update the component with the new status. Please check the following code.

The code is working, but the problem is instead of making one call to api every 5 seconds, it doubles the call every 5 seconds.
0 sec - call api,
5 sec - call api, call api,
10 sec - call api, call api, call api, call api
15 sec - call api x8,
20 sec - call api x16,
...

It seems the reason is that I used an async block in the for_each closure, but gloo_net::Request send() requires running in an async block.

One thing I observed was that in an error scenario, if the deserialization failed on converting to the RunningStatus, then the calling thread errored out and it would not double every 5 seconds. This looks like another hint to solve it, but I could not find a way to stop the thread after finishing the Ok() => {} block.

use std::{fmt::Display};

use futures_util::{stream::StreamExt};
use gloo_net::http::{Request};
use gloo_timers::{
    future::IntervalStream,
};
use serde::Deserialize;
use wasm_bindgen_futures::spawn_local;
use web_sys::console;
use yew::{function_component, html, use_state, Html};

#[derive(Default, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RunningStatus {
    pub is_running: bool,
    pub batch_number: i32,
    pub date_range: DateRange,
}
#[derive(Default, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DateRange {
    pub from: String,
    pub to: String,
}

impl Display for RunningStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "is running {}, batch number: {}, date range {}",
            self.is_running, self.batch_number, self.date_range
        )
    }
}

impl Display for DateRange {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "from {} to {}", self.from, self.to)
    }
}

#[function_component]
pub fn ApiPoller() -> Html {
    let status = use_state(|| RunningStatus::default());
    let status_clone = status.clone();
    spawn_local(async move {
        IntervalStream::new(5_000)
            .for_each(|_| async {
                let status = status_clone.clone();
                let resp = Request::get("/api/v1/process").send().await;
                match resp {
                    Ok(r) => {
                        let rs = r.json::<RunningStatus>().await;
                        match rs {
                            Ok(s) => {
                                status.set(s);
                            }
                            Err(e) => {
                                console::log_1(&e.to_string().into());
                                ()
                            }
                        };
                    }
                    Err(_) => {
                        ()
                    }
                };
            })
            .await;
        ()
    });

    html! {
        <div>
            <p>{ (*status).clone() }</p>
        </div>
    }
}

Every time the state changes, Yew will run your function_component again, and the function will spawn a new polling task which runs along with the existing ones, thus creating the doubling effect.

To fix this, you will need to either:

  • spawn the polling task only once, elsewhere, not inside the ApiPoller function, or
  • keep a boolean state that tracks whether the task has been spawned yet, so that you can spawn it only once.

You may also need some way of cancelling the polling task when the application is no longer looking at it.

I don't actually know how Yew is meant to be used so I can't advise you on which is best — I'm just answering on general principles of this type of programming.

@kpreid, you are absolutely right it needs only to be spawned once. I updated the spawn_local to use the TimeoutFuture instead of IntervalStream it worked perfectly now. Thanks for your help!

    spawn_local(async move {
        TimeoutFuture::new(5_000).await;
        let status = status_clone.clone();
        let resp = Request::get("/api/v1/process").send().await;
        match resp {
            Ok(r) => {
                let rs = r.json::<RunningStatus>().await;
                match rs {
                    Ok(s) => {
                        status.set(s);
                    }
                    Err(e) => {
                        console::log_1(&e.to_string().into());
                        ()
                    }
                };
            }
            Err(_) => (),
        };
    });

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.