Async and Futures
Not all actions complete immediately. Some actions, like a network request, require waiting for system input/output (IO). While waiting for network response, we want to provide status updates, add a loading spinner, and most importantly: avoid blocking the UI thread. Code that blocks the UI thread will prevent further user input, making the UI feel janky and unintuitive.
Rust provides a built-in way of handling asynchronous work with its built-in async/await system. Dioxus provides a first-class integration with Rust's async/await system.
Future: Rust's Async Primitive
The Future
trait is the core of async Rust. A future represents a value that may not yet be ready. In other languages, this is sometimes called a Promise or Task. You can read more about Futures in the Rust book.
We won't cover all the details of futures here, but there are a few important things to know before using them in Dioxus:
- Futures are lazy: They do not do anything until you
await
them orspawn
them. - Futures are concurrent but not always parallel: In Dioxus, all futures run on the main thread.
- Futures pause at await points: You should not hold any locks across those await points.
- Futures can be cancelled before they complete: Your futures need to be "cancel safe."
Futures should be able to handle stopping at any time without panicking or leaving the application in an inconsistent state. They should also be careful not to run blocking operations that lock the main thread.
The lifecycle of a future follows a consistent structure:
- A callback calls an
async fn
or an async closure - The async function returns a Future
- A
dioxus::spawn()
call submits the future to the Dioxus runtime, returning aTask
- The Future is polled in the background until it returns a
Ready
value - If the Future is cancelled, Rust calls its
Drop
implementation
Lazy futures
Unlike JavaScript's Promises, Rust futures are lazy. This means that they do not start executing until you call .await
or start them in the background with spawn
.
This Future will never log "Ran" because it is never awaited:
let future = async { println!("Ran"); };
To run this Future, you can either await it in another Future or spawn it:
let future = async { println!("Ran"); }; let other_future = async { future.await; println!("Ran Other"); }; spawn(other_future);
You can stop polling a Future any time or customize how a Future is polled using the futures crate.
Running Futures with spawn
The Dioxus spawn
function starts running a Future in the background and returns a Task
that you can use to control the Future. It is the basis of all other async hooks in dioxus. You can use spawn to execute one-off tasks in event handlers, hooks or other Futures:
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. 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}" } }
Automatic Cancellation
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:
// Spawn will start a task running in the background which will not be // cancelled when the component is unmounted dioxus::dioxus_core::spawn_forever(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()); } });
Manual Cancellation
If you want to cancel your future manually, you can call the cancel
method on the Task
returned by spawn
or spawn_forever
. This will stop the future from running and drop it.
let mut response = use_signal(|| "Click to start a request".to_string()); let mut task = use_signal(|| None); rsx! { button { onclick: move |_| { response.set("...".into()); // Spawn will start a task running in the background let new_task = spawn(async move { let resp = reqwest::Client::new() .get("https://httpbin.org/delay/1") .send() .await; if resp.is_ok() { response.set("httpbin.org responded!".into()); } else { response.set("failed to fetch response!".into()); } }); task.set(Some(new_task)); }, "{response}" } button { onclick: move |_| { // If the task is running, cancel it if let Some(t) = task.take() { t.cancel(); response.set("Request cancelled".into()); } else { response.set("No request to cancel".into()); } }, "Cancel Request" } }
Cancel Safety
Async tasks can be cancelled at any time. The futures you spawn in dioxus may be canceled:
- When the component they were spawned in is unmounted.
- When the task is cancelled manually using the
cancel
method on theTask
returned byspawn
orspawn_forever
. - When a resource restarts
This means that your futures need to be cancel safe. A cancel-safe future is one that can be stopped at any await point without causing issues. For example, if you are using a global state, you need to ensure that the state is restored when the future is dropped:
static 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:
You can mitigate issues with cancellation by cleaning up resources manually. For example, by making sure global state is restored when the future is dropped:
static 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:
Async methods will often mention if they are cancel safe in their documentation. Generally, most futures you'll encounter when building Dioxus apps are cancel safe.
Concurrency vs Parallelism
Concurrency and parallelism are often confused, but the difference has important implications for how you write your applications. Multiple concurrent tasks may be in progress at the same time, but they don't necessarily run at the same time. In Rust, futures are concurrent. They can yield control to other tasks at await points, allowing other tasks to run while they wait for a value to become ready.
In contrast, multiple parallel tasks can run at exactly the same time on different threads. In Rust, you can spawn parallel tasks using the std::thread
module or libraries like rayon
.
Rust has multiple different async runtimes like tokio
or wasm-bindgen-futures
. Dioxus provides its own async runtime built on top of a platform specific runtime for each renderer. On desktop and mobile, we use Tokio to progress futures.
The Dioxus runtime is single threaded which means futures can use !Send
types, but they need to be careful to never block the thread.
spawn(async { // This will block the main thread and make the UI unresponsive. // Do not do this! solve_for_the_answer_to_life_and_everything(); println!("Ran"); });
If you have an expensive task you need to run, you should spawn it on a separate thread using std::thread::spawn
on desktop/mobile or use a web worker on the web. This will allow the main thread to continue running and keep the UI responsive.
std::thread::spawn(|| { // This will run on a separate thread and not block the main thread. solve_for_the_answer_to_life_and_everything(); println!("Ran"); });
Handling locks
Futures will pause execution at .await
points, allowing other tasks to run until the future is ready to continue. You should never hold read
/ write
locks across .await
points because another async task could try to use the value while the future is paused and the lock is still open. Instead, you need to ensure that locks are only held for the duration of the critical section and released before awaiting.
Long-lived Futures
In some apps, you might want to include long-lived tasks that exist for the lifetime of the app. This might be a background sync engine or a thread listening to some system IO. For these use cases, we provide the spawn_forever
function. This works exactly the same as spawn
, but instead of spawning the future under the current component, the future is attached to the root component. Because the root component is never unmounted, the task continues until the app is closed.
use_hook(|| spawn_forever(async move { println!("Starting a background task!"); }));
This function does have its drawbacks and is meant for advanced use cases. If any resources like a Signal are used in this future, they must also be valid for the lifetime of the app. Using Signals after they have been dropped will lead to a panic and crash your app!