I am building a Yew manga reader application. The home page is a bookshelf that contains thumbnails, and clicking on the thumbnails brings me to a page rendering the pages of the book.
fn switch(routes: Route) -> Html {
match routes {
Route::Home => html! {
<>
<h1>{ "Books" }</h1>
<BookShelf />
</>
},
Route::BookContent { name } => html! {
<div>
<BookContent name={name} />
</div>
},
Route::NotFound => html! { <div>{ "You sure that book exist?" }</div> },
}
}
#[function_component]
fn App() -> Html {
html! {
<HashRouter>
<Switch<Route> render={switch} />
</HashRouter>
}
}
The BookShelf
gets a list of books I have, fetches the thumbnails individually to render in 2 columns.
#[styled_component]
fn BookShelf() -> Html {
let books = use_state(|| vec![]);
{
let books = books.clone();
use_effect_with((), move |_| {
let books = books.clone();
wasm_bindgen_futures::spawn_local(async move {
let url = format!("{}/books", BACKEND_URL);
let fetched_books: Vec<String> = Request::get(&url)
.send()
.await
.unwrap()
.json()
.await
.unwrap();
books.set(fetched_books);
});
})
}
html! {
<>
{
for books.chunks(2).map(|books| html! {
<div class={css!("display:flex;")}>
{
books.iter().map(|book| html! {
<div class={css!("flex:50%;")}>
<Book name={book.clone()}/>
</div>
}).collect::<Html>()
}
</div>
})
}
</>
}
}
The Book
component actually fetches the thumbnail using use_effect_with()
. It also switches route to Route::BookContent
when it is clicked.
BookContent
is similar to BookShelf
except it fetches and renders the images in the same column.
The current behaviour of the page is that when I click a thumbnail before all thumbnails are fetched and loaded, it goes to the BookContent
page, but it waits all the pending thumbnail GET
requests to complete before fetching the book content.
How can I abort all pending get request after the Route::Home
component disappears (aka clicking a thumbnail to go to Route::BookContent
) so that I can immediately start fetching the book content?
I just came across Theo's video "You Need React Query Now More Than Ever", which mentions you can send signal to useQuery
and abort the query. I am thinking this may be want I need, to early return the callback if route is changed. Probably using a combination of context and use_location
/ use_route
?
So actually the future that I should be aborting is gloo_net::http::Request
not wasm_bindgen_futures::spwan_local
because the wasm future gets dispatched immediately what it is rendered and it is the Request
that is awaiting. And luckily there is an abort mechanism in gloo_net::http::Request
by setting AbortSignal
.
By making use of the fact that use_effect_with()
calls its return value to cleanup when a component is destroyed, we can abort Request
when Image
component gets out of scope.
use gloo_net::http::Request;
use web_sys::AbortController;
#[derive(Properties, PartialEq)]
struct ImageProperties {
url: String,
}
#[function_component]
fn Image(props: &ImageProperties) -> Html {
let image = use_state(|| String::new());
{
let image = image.clone();
let url = props.url.clone();
let controller = AbortController::new().unwrap();
let signal = controller.signal();
use_effect_with((), move |_| {
wasm_bindgen_futures::spawn_local(async move {
let res = match Request::get(&url)
.abort_signal(Some(&signal))
.send()
.await
{
Ok(res) => res,
_ => return,
};
let type_ = res.headers().get("Content-Type").unwrap();
let bytes = res.binary().await.unwrap();
let data = b64.encode(bytes);
image.set(format!("data:{};base64,{}", type_, data));
});
move || {
controller.abort();
}
});
}
html! {
<img
src={(*image).clone()}
alt={"loading"}
style="height:auto;width:100%;object-fit:inherit"
/>
}
}
Note that if you renders an image with
html! {
html! {
<img src={url} alt={"loading"}/>
}
}
The GET
request dispatched by img
tag won't be aborted even when the component is destroyed.