Function freya::prelude::use_coroutine
pub fn use_coroutine<M, G, F>(init: G) -> Coroutine<M>
Expand description
Maintain a handle over a future that can be paused, resumed, and canceled.
This is an upgraded form of crate::use_future()
with an integrated channel system.
Specifically, the coroutine generated here comes with an futures_channel::mpsc::UnboundedSender
built into it - saving you the hassle of building your own.
Additionally, coroutines are automatically injected as shared contexts, so downstream components can tap into a coroutine’s channel and send messages into a singular async event loop.
This makes it effective for apps that need to interact with an event loop or some asynchronous code without thinking too hard about state.
§Global State
Typically, writing apps that handle concurrency properly can be difficult, so the intention of this hook is to make it easy to join and poll async tasks concurrently in a centralized place. You’ll find that you can have much better control over your app’s state if you centralize your async actions, even under the same concurrent context. This makes it easier to prevent undeseriable states in your UI while various async tasks are already running.
This hook is especially powerful when combined with Fermi. We can store important global data in a coroutine, and then access display-level values from the rest of our app through atoms.
§UseCallback instead
However, you must plan out your own concurrency and synchronization. If you
don’t care about actions in your app being synchronized, you can use crate::use_callback()
hook to spawn multiple tasks and run them concurrently.
§Notice
In order to use rx.next().await
, you will need to extend the Stream
trait (used by UnboundedReceiver
)
by adding the futures-util
crate as a dependency and adding StreamExt
into scope via use futures_util::stream::StreamExt;
§Example
use futures_util::StreamExt;
enum Action {
Start,
Stop,
}
let chat_client = use_coroutine(|mut rx: UnboundedReceiver<Action>| async move {
while let Some(action) = rx.next().await {
match action {
Action::Start => {}
Action::Stop => {},
}
}
});
rsx!{
button {
onclick: move |_| chat_client.send(Action::Start),
"Start Chat Service"
}
};
§Additional Information that may be useful
This function is a hook which means you need to follow the rules of hooks when you call it. You can click here to learn more about the rules of hooks.
Hooks in dioxus need to run in the same order every time you run the component. To make sure you run hooks in a consistent order, you should follow the rules of hooks:
- Hooks should only be called from the root of a component or another hook
fn App() -> Element {
// ✅ You can call hooks from the body of a component
let number = use_signal(|| 1);
if number() == 1 {
// ❌ You can run into issues if you can hooks inside other expressions inside your component
// If number changes from 0 to 1, the order of the hooks will be different and your app may panic
let string = use_signal(|| "hello world".to_string());
}
todo!()
}
fn use_my_hook() -> Signal<i32> {
// ✅ You can call hooks from the body of other hooks
let number = use_signal(|| 1);
// ❌ Again, creating hooks inside expressions inside other hooks can cause issues
if number() == 1 {
let string = use_signal(|| "hello world".to_string());
}
number
}
- Hooks should always start with
use_
to make it clear that you need to call them in a consistent order
Because hooks should only be called from the root of a component or another hook, you shouldn’t call hooks inside of:
- ❌ Conditionals
fn App() -> Element {
let number = use_signal(|| 1);
// ❌ Changing the condition will change the order of the hooks
if number() == 1 {
let string = use_signal(|| "hello world".to_string());
}
// ❌ Changing the value you are matching will change the order of the hooks
match number() {
1 => {
let string = use_signal(|| "hello world".to_string());
},
_ => (),
}
todo!()
}
- ❌ Loops
fn App() -> Element {
let number = use_signal(|| 1);
// ❌ Changing the loop will change the order of the hooks
for i in 0..number() {
let string = use_signal(|| "hello world".to_string());
}
todo!()
}
- ❌ Event Handlers
fn App() -> Element {
rsx! {
button {
onclick: move |_| {
// ❌ Calling the event handler will change the order of the hooks
use_signal(|| "hello world".to_string());
},
"Click me"
}
}
}
- ❌ Initialization closures in other hooks
fn App() -> Element {
let number = use_signal(|| {
// ❌ This closure will only be called when the component is first created. Running the component will change the order of the hooks
let string = use_signal(|| "hello world".to_string());
string()
});
todo!()
}
Why do hooks need to run in a consistent order?
Hooks need to be run in a consistent order because dioxus stores hooks in a list and uses the order you run hooks in to determine what part of the state belongs to which hook.
Lets look at an example component:
fn App() -> Element {
let number = use_signal(|| 1); // Hook 1
let string = use_signal(|| "hello world".to_string()); // Hook 2
let doubled = use_memo(move || number() * 2); // Hook 3
todo!()
}
When we first create the component, we run the hooks in the order they are defined and store the state in the component in a list.
[
Box::new(1),
Box::new("hello world".to_string()),
Box::new(2),
]
Next time we run the component, we return items from the state list instead of creating state again.
[
Box::new(1), // Hook 1 returns 1
Box::new("hello world".to_string()), // Hook 2 returns "hello world"
Box::new(2), // Hook 3 returns 2
]
The order the hooks are run it must be the same because the order determines which hook gets what state! If you run the hooks in a different order, dioxus may panic because it can’t turn the state back into the right type or you may just get the wrong state for your hook.