|
@@ -1 +1,161 @@
|
|
|
# Coroutines
|
|
|
+
|
|
|
+Another good tool to keep in your async toolbox are coroutines. Coroutines are futures that can be manually stopped, started, paused, and resumed.
|
|
|
+
|
|
|
+Like regular futures, code in a Dioxus coroutine will run until the next `await` point before yielding. This low-level control over asynchronous tasks is quite powerful, allowing for infinitely looping tasks like WebSocket polling, background timers, and other periodic actions.
|
|
|
+
|
|
|
+## `use_coroutine`
|
|
|
+
|
|
|
+The basic setup for coroutines is the `use_coroutine` hook. Most coroutines we write will be polling loops using async/await.
|
|
|
+
|
|
|
+```rust
|
|
|
+fn app(cx: Scope) -> Element {
|
|
|
+ let ws: &UseCoroutine<()> = use_coroutine(&cx, |rx| async move {
|
|
|
+ // Connect to some sort of service
|
|
|
+ let mut conn = connect_to_ws_server().await;
|
|
|
+
|
|
|
+ // Wait for data on the service
|
|
|
+ while let Some(msg) = conn.next().await {
|
|
|
+ // handle messages
|
|
|
+ }
|
|
|
+ });
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+For many services, a simple async loop will handle the majority of use cases.
|
|
|
+
|
|
|
+However, if we want to temporarily disable the coroutine, we can "pause" it using the `pause` method, and "resume" it using the `resume` method:
|
|
|
+
|
|
|
+```rust
|
|
|
+let sync: &UseCoroutine<()> = use_coroutine(&cx, |rx| async move {
|
|
|
+ // code for syncing
|
|
|
+});
|
|
|
+
|
|
|
+if sync.is_running() {
|
|
|
+ cx.render(rsx!{
|
|
|
+ button {
|
|
|
+ onclick: move |_| sync.pause(),
|
|
|
+ "Disable syncing"
|
|
|
+ }
|
|
|
+ })
|
|
|
+} else {
|
|
|
+ cx.render(rsx!{
|
|
|
+ button {
|
|
|
+ onclick: move |_| sync.resume(),
|
|
|
+ "Enable syncing"
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+This pattern is where coroutines are extremely useful - instead of writing all the complicated logic for pausing our async tasks like we would with JavaScript promises, the Rust model allows us to just not poll our future.
|
|
|
+
|
|
|
+## Sending Values
|
|
|
+
|
|
|
+You might've noticed the `use_coroutine` closure takes an argument called `rx`. What is that? Well, a common pattern in complex apps is to handle a bunch of async code at once. With libraries like Redux Toolkit, managing multiple promises at once can be challenging and a common source of bugs.
|
|
|
+
|
|
|
+With Coroutines, we have the opportunity to centralize our async logic. The `rx` parameter is an Unbounded Channel for code external to the coroutine to send data *into* the coroutine. Instead of looping on an external service, we can loop on the channel itself, processing messages from within our app without needing to spawn a new future. To send data into the coroutine, we would call "send" on the handle.
|
|
|
+
|
|
|
+
|
|
|
+```rust
|
|
|
+enum ProfileUpdate {
|
|
|
+ SetUsername(String),
|
|
|
+ SetAge(i32)
|
|
|
+}
|
|
|
+
|
|
|
+let profile = use_coroutine(&cx, |mut rx: UnboundedReciver<ProfileUpdate>| async move {
|
|
|
+ let mut server = connect_to_server().await;
|
|
|
+
|
|
|
+ while let Ok(msg) = rx.next().await {
|
|
|
+ match msg {
|
|
|
+ ProfileUpdate::SetUsername(name) => server.update_username(name).await,
|
|
|
+ ProfileUpdate::SetAge(age) => server.update_age(age).await,
|
|
|
+ }
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+
|
|
|
+cx.render(rsx!{
|
|
|
+ button {
|
|
|
+ onclick: move |_| profile.send(ProfileUpdate::SetUsername("Bob".to_string())),
|
|
|
+ "Update username"
|
|
|
+ }
|
|
|
+})
|
|
|
+```
|
|
|
+
|
|
|
+For sufficiently complex apps, we could build a bunch of different useful "services" that loop on channels to update the app.
|
|
|
+
|
|
|
+```rust
|
|
|
+let profile = use_coroutine(&cx, profile_service);
|
|
|
+let editor = use_coroutine(&cx, editor_service);
|
|
|
+let sync = use_coroutine(&cx, sync_service);
|
|
|
+
|
|
|
+async fn profile_service(rx: UnboundedReceiver<ProfileCommand>) {
|
|
|
+ // do stuff
|
|
|
+}
|
|
|
+
|
|
|
+async fn sync_service(rx: UnboundedReceiver<SyncCommand>) {
|
|
|
+ // do stuff
|
|
|
+}
|
|
|
+
|
|
|
+async fn editor_service(rx: UnboundedReceiver<EditorCommand>) {
|
|
|
+ // do stuff
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+We can combine coroutines with Fermi to emulate Redux Toolkit's Thunk system with much less headache. This lets us store all of our app's state *within* a task and then simply update the "view" values stored in Atoms. It cannot be understated how powerful this technique is: we get all the perks of native Rust tasks with the optimizations and ergonomics of global state. This means your *actual* state does not need to be tied up in a system like Fermi or Redux - the only Atoms that need to exist are those that are used to drive the display/UI.
|
|
|
+
|
|
|
+```rust
|
|
|
+static USERNAME: Atom<String> = |_| "default".to_string();
|
|
|
+
|
|
|
+fn app(cx: Scope) -> Element {
|
|
|
+ let atoms = use_atom_root(&cx);
|
|
|
+
|
|
|
+ use_coroutine(&cx, |rx| sync_service(rx, atoms.clone()));
|
|
|
+
|
|
|
+ cx.render(rsx!{
|
|
|
+ Banner {}
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+fn Banner(cx: Scope) -> Element {
|
|
|
+ let username = use_read(&cx, USERNAME);
|
|
|
+
|
|
|
+ cx.render(rsx!{
|
|
|
+ h1 { "Welcome back, {username}" }
|
|
|
+ })
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+Now, in our sync service, we can structure our state however we want. We only need to update the view values when ready.
|
|
|
+
|
|
|
+```rust
|
|
|
+enum SyncMsg {
|
|
|
+ SetUsername(String),
|
|
|
+}
|
|
|
+
|
|
|
+async fn sync_service(mut rx: UnboundedReceiver<SyncMsg>, atoms: AtomRoot) {
|
|
|
+ let username = atoms.write(USERNAME);
|
|
|
+ let errors = atoms.write(ERRORS);
|
|
|
+
|
|
|
+ while let Ok(msg) = rx.next().await {
|
|
|
+ match msg {
|
|
|
+ SyncMsg::SetUsername(name) => {
|
|
|
+ if set_name_on_server(&name).await.is_ok() {
|
|
|
+ username.set(name);
|
|
|
+ } else {
|
|
|
+ errors.make_mut().push("SetUsernameFailed");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+## Yielding Values
|
|
|
+
|
|
|
+To yield values from a coroutine, simply bring in a `UseState` handle and set the value whenever your coroutine completes its work.
|
|
|
+
|
|
|
+## Automatic injection into the Context API
|
|
|
+
|
|
|
+Coroutine handles are automatically injected through the context API. `use_coroutine_handle` with the message type as a generic can be used to fetch a handle.
|