docs: finish async

This commit is contained in:
Jonathan Kelley 2022-03-09 10:22:12 -05:00
parent be2b4d36de
commit 3e9023b131
7 changed files with 310 additions and 10 deletions

View file

@ -30,11 +30,11 @@
- [Fermi](state/fermi.md)
- [Router](state/router.md)
- [Working with Async](async/index.md)
- [Updating State](async/loading_state.md)
- [Fetching](async/fetching.md)
- [UseFuture](async/use_future.md)
- [UseCoroutine](async/coroutines.md)
- [WebSockets](async/sockets.md)
<!-- - [Fetching](async/fetching.md) -->
<!-- - [Updating State](async/loading_state.md)
- [WebSockets](async/sockets.md) -->
<!-- - [Tasks](async/asynctasks.md) -->
<!--

View file

@ -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.

View file

@ -1,4 +1,2 @@
# Fetching
This section is currently under construction! 🏗

View file

@ -1 +1,85 @@
# UseFuture
When dealing with asynchronous code, you might need to wait for some action to complete before rendering your component. If you had to build this abstraction yourself, you'd probably end up with some `use_state` spaghetti code.
One of the core hooks that Dioxus provides is `use_future` - a simple hook that lets you tap into a running task.
## Use case
The simplest use case of `use_future` is to prevent rendering until some asynchronous code has been completed. Dioxus doesn't currently have a library as sophisticated as React Query for prefetching tasks, but we can get some of the way there with `use_future`. In one of the Dioxus examples, we use `use_future` to download some search data before rendering the rest of the app:
```rust
fn app(cx: Scope) -> Element {
// set "breeds" to the current value of the future
let breeds = use_future(&cx, (), |_| async move {
reqwest::get("https://dog.ceo/api/breeds/list/all")
.await
.unwrap()
.json::<ListBreeds>()
.await
});
let status = match breeds.value() {
Some(Ok(val)) => "ready!",
Some(Err(err)) => "errored!",
None => "loading!",
}
}
```
On first run, the code inside `use_future` will be submitted to the Dioxus scheduler once the component has rendered. Since there's no data ready when the component loads the first time, its "value" will be `None`.
However, once the future is finished, the component will be re-rendered and a new screen will be displayed - Ok or Err, depending on the outcome of our fetch.
## Restarting the Future
The example we showed above will only ever run once. What happens if some value changed on the server and we need to update our future's value?
Well, the UseFuture handle provides a handy "restart" method. We can wire this up to a button or some other comparison code to get a regenerating future.
```rust
fn app(cx: Scope) -> Element {
// set "breeds" to the current value of the future
let dog = use_future(&cx, (), |_| async move {
reqwest::get("https://dog.ceo/api/breeds/image/random")
.await
.unwrap()
.json::<RandomDog>()
.await
});
cx.render(match breeds.value() {
Some(Ok(val)) => rsx!(div {
img { src: "{val.message}"}
button {
onclick: move |_| dog.restart(),
"Click to fetch a new dog"
}
}),
Some(Err(err)) => rsx!("Failed to load dog"),
None => rsx!("Loading dog image!"),
})
}
```
## With Dependencies
We showed that UseFuture can be regenerated manually, but how can we automatically get it to update whenever some input value changes? This is where the "dependencies" tuple comes into play. We just need to add a value into our tuple argument and it'll be automatically cloned into our future when it starts.
```rust
#[inline_props]
fn RandomDog(cx: Scope, breed: String) -> Element {
let dog = use_future(&cx, (breed,), |(breed)| async move {
reqwest::get(format!("https://dog.ceo/api/breed/{breed}/images/random"))
.await
.unwrap()
.json::<RandomDog>()
.await
});
// some code as before
}
```

View file

@ -41,7 +41,7 @@ Whenever we visit this app, we will get either the Home component or the Blog co
We can fix this one of two ways:
- A fallback 404 page
-
```rust
rsx!{
Router {

View file

@ -1,7 +1,65 @@
# Global State
If your app has finally gotten large enough where passing values through the tree ends up polluting the intent of your code, then it might be time to turn to global state.
cx.provide_context()
cx.consume_context()
In Dioxus, global state is shared through the Context API. This guide will show you how to use the Context API to simplify state management.
> This section is currently under construction! 🏗
## Provide Context and Consume Context
The simplest way of retrieving shared state in your app is through the Context API. The Context API allows you to provide and consume an item of state between two components.
Whenever a component provides a context, it is then accessible to any child components.
> Note: parent components cannot "reach down" and consume state from below their position in the tree.
The terminology here is important: `provide` context means that the component will expose state and `consume` context means that the child component can acquire a handle to state above it.
Instead of using keys or statics, Dioxus prefers the `NewType` pattern to search for parent state. This means each state you expose as a context should be its own unique type.
In practice, you'll have a component that exposes some state:
```rust
#[derive(Clone)]
struct Title(String);
fn app(cx: Scope) -> Element {
cx.use_hook(|_| {
cx.provide_context(Title("Hello".to_string()));
});
cx.render(rsx!{
Child {}
})
}
```
And then in our component, we can consume this state at any time:
```rust
fn Child(cx: Scope) -> Element {
let name = cx.consume_context::<Title>();
//
}
```
Note: calling "consume" state can be a rather expensive operation to perform during each render. Prefer to consume state within a `use_hook`:
```rust
fn Child(cx: Scope) -> Element {
// cache our "consume_context" operation
let name = cx.use_hook(|_| cx.consume_context::<Title>());
}
```
All `Context` must be cloned - the item will be cloned into each call of `consume_context`. To make this operation cheaper, consider wrapping your type in an `Rc` or `Arc`.
<!-- ## Coroutines
The `use_coroutine` hook -->
<!-- # `use_context` and `use_context_provider`
These -->

View file

@ -36,7 +36,7 @@ use std::future::Future;
///
/// ## Example
///
/// ```rust, ignore
/// ```rust
/// enum Action {
/// Start,
/// Stop,