Handling Asynchronous Tasks
Asynchronous tasks are a core part of any modern application. Dioxus provides a few different methods to handle asynchronous tasks. This guide will cover how to use each of them. If you already know what kind of asynchronous task you need, you can skip to the section for that task:
- spawn is great for futures you need to run in the background that don't return a value
- use_resource handles asynchronous state while retaining control of exactly what happens while the future is running
- It can be combined with Suspense to handle many pending tasks with the same loading view
Running Futures with spawn
The spawn
method spawns a future in the background and returns a Task
that you can use to cancel the future. Spawn is great for futures you want to start and then forget about like sending analytics data to a server:
let mut response = use_signal(|| "Click to start a request".to_string()); rsx! { button { onclick: move |_| { response.set("...".into()); // Spawn will start a task running in the background spawn(async move { let resp = reqwest::Client::new() .get("https://dioxuslabs.com") .send() .await; if resp.is_ok() { response.set("dioxuslabs.com responded!".into()); } else { response.set("failed to fetch response!".into()); } }); }, "{response}" } }
Since spawning in event handlers is very common, Dioxus provides a more concise syntax for async event handlers. If you return a future from an event handler, Dioxus will automatically spawn
it:
let mut response = use_signal(|| "Click to start a request".to_string()); rsx! { button { // Async closures passed to event handlers are automatically spawned onclick: move |_| async move { response.set("...".into()); let resp = reqwest::Client::new() .get("https://dioxuslabs.com") .send() .await; if resp.is_ok() { response.set("dioxuslabs.com responded!".into()); } else { response.set("failed to fetch response!".into()); } }, "{response}" } }
The future you pass to the spawn
will automatically be cancelled when the component is unmounted. If you need to keep the future running until it is finished, you can use spawn_forever
instead.
Asynchronous State with use_resource
The use_resource
can be used to derive asynchronous state. It takes an async closure to calculate the state and returns a tracked value with the current state of the future. Any time a dependency of the resource changes, the resource will rerun:
let mut breed = use_signal(|| "hound".to_string()); let dogs = use_resource(move || async move { reqwest::Client::new() // Since breed is read inside the async closure, the resource will subscribe to the signal // and rerun when the breed is written to .get(format!("https://dog.ceo/api/breed/{breed}/images")) .send() .await? .json::<BreedResponse>() .await }); rsx! { input { value: "{breed}", // When the input is changed and the breed is set, the resource will rerun oninput: move |evt| breed.set(evt.value()), } div { display: "flex", flex_direction: "row", // You can read resource just like a signal. If the resource is still // running, it will return None if let Some(response) = &*dogs.read() { match response { Ok(urls) => rsx! { for image in urls.iter().take(3) { img { src: "{image}", width: "100px", height: "100px", } } }, Err(err) => rsx! { "Failed to fetch response: {err}" }, } } else { "Loading..." } } }
The use_resource
hook might look similar to the use_memo
hook, but unlike use_memo
, the resource's output is not memoized with PartialEq
. That means any components/reactive hooks that read the output will rerun if the future reruns even if the value it returns is the same:
let mut number = use_signal(|| 0); // Resources rerun any time their dependencies change. They will // rerun any reactive scopes that read the resource when they finish // even if the value hasn't changed let halved_resource = use_resource(move || async move { number() / 2 }); log!("Component reran"); rsx! { button { onclick: move |_| number += 1, "Increment" } p { if let Some(halved) = halved_resource() { "Halved: {halved}" } else { "Loading..." } } }
Loading...
Note: The future you pass to
use_resource
must be cancel safe. Cancel safe futures are futures that can be stopped at any await point without causing causing issues. For example, this task is not cancel safe:src/asynchronous.rsstatic RESOURCES_RUNNING: GlobalSignal<HashSet<String>> = Signal::global(|| HashSet::new()); let mut breed = use_signal(|| "hound".to_string()); let dogs = use_resource(move || async move { // Modify some global state RESOURCES_RUNNING.write().insert(breed()); // Wait for a future to finish. The resource may cancel // without warning if breed is changed while the future is running. If // it does, then the breed pushed to RESOURCES_RUNNING will never be popped let response = reqwest::Client::new() .get(format!("https://dog.ceo/api/breed/{breed}/images")) .send() .await? .json::<BreedResponse>() .await; // Restore some global state RESOURCES_RUNNING.write().remove(&breed()); response });RESOURCES_RUNNING:
Loading...It can be fixed by making sure the global state is restored when the future is dropped:
src/asynchronous.rsstatic RESOURCES_RUNNING: GlobalSignal<HashSet<String>> = Signal::global(|| HashSet::new()); let mut breed = use_signal(|| "hound".to_string()); let dogs = use_resource(move || async move { // Modify some global state RESOURCES_RUNNING.write().insert(breed()); // Automatically restore the global state when the future is dropped, even if // isn't finished struct DropGuard(String); impl Drop for DropGuard { fn drop(&mut self) { RESOURCES_RUNNING.write().remove(&self.0); } } let _guard = DropGuard(breed()); // Wait for a future to finish. The resource may cancel // without warning if breed is changed while the future is running. If // it does, then it will be dropped and the breed will be popped reqwest::Client::new() .get(format!("https://dog.ceo/api/breed/{breed}/images")) .send() .await? .json::<BreedResponse>() .await });RESOURCES_RUNNING:
Loading...Async methods will often mention if they are cancel safe in their documentation.
Unified Loading Views with suspense
SuspenseBoundary
is a convenient way to bundle multiple async tasks into a single loading view. It accepts a loading closure and children. You can suspend tasks in children to pause rendering of that child until the future is finished. The suspense boundary will show the loading view instead of the children while any of its children are suspended. Once that suspense is resolved, it will show the children again.
We can use a suspense boundary to show a grid of different breeds of dogs without handling each loading state individually:
fn DogGrid() -> Element { rsx! { SuspenseBoundary { // When any child components (like BreedGallery) are suspended, this closure will // be called and the loading view will be rendered instead of the children fallback: |_| rsx! { div { width: "100%", height: "100%", display: "flex", align_items: "center", justify_content: "center", "Loading..." } }, div { display: "flex", flex_direction: "column", BreedGallery { breed: "hound" } BreedGallery { breed: "poodle" } BreedGallery { breed: "beagle" } } } } } #[component] fn BreedGallery(breed: ReadOnlySignal<String>) -> Element { let response = use_resource(move || async move { // Artificially slow down the request to make the loading indicator easier to seer gloo_timers::future::TimeoutFuture::new(1000).await; reqwest::Client::new() .get(format!("https://dog.ceo/api/breed/{breed}/images")) .send() .await? .json::<BreedResponse>() .await }) // Calling .suspend()? will suspend the component and return early while the future is running .suspend()?; // Then you can just handle the happy path with the resolved future rsx! { div { display: "flex", flex_direction: "row", match &*response.read() { Ok(urls) => rsx! { for image in urls.iter().take(3) { img { src: "{image}", width: "100px", height: "100px", } } }, Err(err) => rsx! { "Failed to fetch response: {err}" }, } } } }
If you need to change the loading view while a specific task is loading, you can provide a different loading view with the with_loading_placeholder
method. The loading placeholder you return from the method will be passed to the suspense boundary and may choose to render it instead of the default loading view:
fn DogGrid() -> Element { rsx! { SuspenseBoundary { // The fallback closure accepts a SuspenseContext which contains // information about the suspended component fallback: |suspense_context: SuspenseContext| if let Some(view) = suspense_context.suspense_placeholder() { view } else { rsx! { div { width: "100%", height: "100%", display: "flex", align_items: "center", justify_content: "center", "Loading..." } } }, div { display: "flex", flex_direction: "column", BreedGallery { breed: "hound" } BreedGallery { breed: "poodle" } BreedGallery { breed: "beagle" } } } } } #[component] fn BreedGallery(breed: ReadOnlySignal<String>) -> Element { let response = use_resource(move || async move { gloo_timers::future::TimeoutFuture::new(breed().len() as u32 * 100).await; reqwest::Client::new() .get(format!("https://dog.ceo/api/breed/{breed}/images")) .send() .await? .json::<BreedResponse>() .await }) .suspend() // You can pass up a loading placeholder to the nearest SuspenseBoundary // with the with_loading_placeholder method .with_loading_placeholder(move || { rsx! { div { width: "100%", height: "100%", display: "flex", align_items: "center", justify_content: "center", "Loading {breed}..." } } })?; // Then you can just handle the happy path with the resolved future rsx! { div { display: "flex", flex_direction: "row", match &*response.read() { Ok(urls) => rsx! { for image in urls.iter().take(3) { img { src: "{image}", width: "100px", height: "100px", } } }, Err(err) => rsx! { "Failed to fetch response: {err}" }, } } } }
Suspense with Fullstack
To use suspense in your fullstack application, you need to use the use_server_future
hook instead of use_resource
. use_server_future
handles serialization of the result of the future for hydration. It will also suspend automatically, so you don't need to call .suspend()
on the future.
#[component] fn BreedGallery(breed: ReadOnlySignal<String>) -> Element { // use_server_future is very similar to use_resource, but the value returned from the future // must implement Serialize and Deserialize and it is automatically suspended let response = use_server_future(move || async move { // The future will run on the server during SSR and then get sent to the client reqwest::Client::new() .get(format!("https://dog.ceo/api/breed/{breed}/images")) .send() .await // reqwest::Result does not implement Serialize, so we need to map it to a string which // can be serialized .map_err(|err| err.to_string())? .json::<BreedResponse>() .await .map_err(|err| err.to_string()) // use_server_future calls `suspend` internally, so you don't need to call it manually, but you // do need to bubble up the suspense variant with `?` })?; // If the future was still pending, it would have returned suspended with the `?` above // we can unwrap the None case here to get the inner result let response_read = response.read(); let response = response_read.as_ref().unwrap(); // Then you can just handle the happy path with the resolved future rsx! { div { display: "flex", flex_direction: "row", match response { Ok(urls) => rsx! { for image in urls.iter().take(3) { img { src: "{image}", width: "100px", height: "100px", } } }, Err(err) => rsx! { "Failed to fetch response: {err}" }, } } } }
![](https://images.dog.ceo/breeds/hound-afghan/n02088094_1003.jpg)
![](https://images.dog.ceo/breeds/hound-afghan/n02088094_10263.jpg)
![](https://images.dog.ceo/breeds/hound-afghan/n02088094_10715.jpg)
![](https://images.dog.ceo/breeds/poodle-medium/PXL_20210220_100624962.jpg)
![](https://images.dog.ceo/breeds/poodle-medium/WhatsApp_Image_2022-08-06_at_4.48.38_PM.jpg)
![](https://images.dog.ceo/breeds/poodle-miniature/242981_4709618542275_742310792_o.jpg)
![](https://images.dog.ceo/breeds/beagle/1271553739_Milo.jpg)
![](https://images.dog.ceo/breeds/beagle/1374053345_Milo.jpg)
![](https://images.dog.ceo/breeds/beagle/166407056_Milo.jpg)
Unlike use_resource
, use_server_future
is only reactive in the closure, not the future itself. If you need to subscribe to another reactive value, you need to read it in the closure before passing it to the future:
let id = use_signal(|| 0); // ❌ The future inside of use_server_future is not reactive use_server_future(move || { async move { // But the future is not reactive which means that the future will not subscribe to any reads here println!("{id}"); } }); // ✅ The closure that creates the future for use_server_future is reactive use_server_future(move || { // The closure itself is reactive which means the future will subscribe to any signals you read here let cloned_id = id(); async move { // But the future is not reactive which means that the future will not subscribe to any reads here println!("{cloned_id}"); } });
When you use suspense with fullstack without streaming enabled, dioxus will wait until all suspended futures are resolved before sending the resolved html to the client. If you enable out of order streaming, dioxus will send the finished HTML chunks to the client one at a time as they are resolved:
fn main() { dioxus::LaunchBuilder::new() .with_context(server_only! { // Enable out of order streaming during SSR dioxus::fullstack::ServeConfig::builder().enable_out_of_order_streaming() }) .launch(DogGrid); }
Conclusion
This guide has covered the basics of asynchronous tasks in Dioxus. More detailed documentation about specific hooks are available in docs.rs:
More examples of futures and asynchronous tasks are available in the example folder in the dioxus repo.