Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Overhaul async hooks: use_future, use_coroutine, use_effect (new) #161

Merged
merged 14 commits into from
Feb 26, 2022
Merged
38 changes: 13 additions & 25 deletions examples/dog_app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,20 @@ struct ListBreeds {
}

fn app(cx: Scope) -> Element {
let breeds = use_future(&cx, || async move {
let (breed, set_breed) = use_state(&cx, || None);

let breeds = use_future(&cx, (), |_| async move {
reqwest::get("https://dog.ceo/api/breeds/list/all")
.await
.unwrap()
.json::<ListBreeds>()
.await
});

let (breed, set_breed) = use_state(&cx, || None);

match breeds.value() {
Some(Ok(breeds)) => cx.render(rsx! {
div {
h1 {"Select a dog breed!"}

h1 { "Select a dog breed!" }
div { display: "flex",
ul { flex: "50%",
breeds.message.keys().map(|breed| rsx!(
Expand All @@ -51,34 +50,23 @@ fn app(cx: Scope) -> Element {
}
}
}),
Some(Err(_e)) => cx.render(rsx! {
div { "Error fetching breeds" }
}),
None => cx.render(rsx! {
div { "Loading dogs..." }
}),
Some(Err(_e)) => cx.render(rsx! { div { "Error fetching breeds" } }),
None => cx.render(rsx! { div { "Loading dogs..." } }),
}
}

#[derive(serde::Deserialize, Debug)]
struct DogApi {
message: String,
}

#[inline_props]
fn Breed(cx: Scope, breed: String) -> Element {
#[derive(serde::Deserialize, Debug)]
struct DogApi {
message: String,
}

let endpoint = format!("https://dog.ceo/api/breed/{}/images/random", breed);

let fut = use_future(&cx, || async move {
let fut = use_future(&cx, (breed,), |(breed,)| async move {
let endpoint = format!("https://dog.ceo/api/breed/{}/images/random", breed);
reqwest::get(endpoint).await.unwrap().json::<DogApi>().await
});

let (name, set_name) = use_state(&cx, || breed.clone());
if name != breed {
set_name(breed.clone());
fut.restart();
}

cx.render(match fut.value() {
Some(Ok(resp)) => rsx! {
button {
Expand Down
6 changes: 3 additions & 3 deletions examples/suspense.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ fn app(cx: Scope) -> Element {
div {
h1 {"Dogs are very important"}
p {
"The dog or domestic dog (Canis familiaris[4][5] or Canis lupus familiaris[5])"
"is a domesticated descendant of the wolf which is characterized by an upturning tail."
"The dog or domestic dog (Canis familiaris[4][5] or Canis lupus familiaris[5])"
"is a domesticated descendant of the wolf which is characterized by an upturning tail."
"The dog derived from an ancient, extinct wolf,[6][7] and the modern grey wolf is the"
"dog's nearest living relative.[8] The dog was the first species to be domesticated,[9][8]"
"by hunter–gatherers over 15,000 years ago,[7] before the development of agriculture.[1]"
Expand All @@ -52,7 +52,7 @@ fn app(cx: Scope) -> Element {
/// Suspense is achieved my moving the future into only the component that
/// actually renders the data.
fn Doggo(cx: Scope) -> Element {
let fut = use_future(&cx, || async move {
let fut = use_future(&cx, (), |_| async move {
reqwest::get("https://dog.ceo/api/breeds/image/random/")
.await
.unwrap()
Expand Down
4 changes: 2 additions & 2 deletions examples/tasks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ fn main() {
fn app(cx: Scope) -> Element {
let (count, set_count) = use_state(&cx, || 0);

use_future(&cx, move || {
let set_count = set_count.to_owned();
use_future(&cx, (), move |_| {
let set_count = set_count.clone();
async move {
loop {
tokio::time::sleep(Duration::from_millis(1000)).await;
Expand Down
7 changes: 7 additions & 0 deletions packages/hooks/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,10 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]

[dependencies]
dioxus-core = { path = "../../packages/core", version = "^0.1.9" }
futures-channel = "0.3.21"
log = { version = "0.4", features = ["release_max_level_off"] }


[dev-dependencies]
futures-util = { version = "0.3", default-features = false }
dioxus-core = { path = "../../packages/core", version = "^0.1.9" }
4 changes: 2 additions & 2 deletions packages/hooks/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ pub use usecoroutine::*;
mod usefuture;
pub use usefuture::*;

mod usesuspense;
pub use usesuspense::*;
mod useeffect;
pub use useeffect::*;

#[macro_export]
/// A helper macro for using hooks in async environements.
Expand Down
220 changes: 121 additions & 99 deletions packages/hooks/src/usecoroutine.rs
Original file line number Diff line number Diff line change
@@ -1,122 +1,144 @@
use dioxus_core::{ScopeState, TaskId};
pub use futures_channel::mpsc::{UnboundedReceiver, UnboundedSender};
use std::future::Future;
use std::{cell::Cell, rc::Rc};
/*



let g = use_coroutine(&cx, || {
// clone the items in
async move {

}
})



*/
pub fn use_coroutine<F>(cx: &ScopeState, create_future: impl FnOnce() -> F) -> CoroutineHandle<'_>
use std::rc::Rc;

/// Maintain a handle over a future that can be paused, resumed, and canceled.
///
/// This is an upgraded form of [`use_future`] with an integrated channel system.
/// Specifically, the coroutine generated here comes with an [`UnboundedChannel`]
/// built into it - saving you the hassle of building your own.
///
/// Addititionally, 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 [`use_callback`]
/// hook to spawn multiple tasks and run them concurrently.
///
/// ## Example
///
/// ```rust, ignore
/// enum Action {
/// Start,
/// Stop,
/// }
///
/// let chat_client = use_coroutine(&cx, |rx: UnboundedReceiver<Action>| async move {
/// while let Some(action) = rx.next().await {
/// match action {
/// Action::Start => {}
/// Action::Stop => {},
/// }
/// }
/// });
///
///
/// cx.render(rsx!{
/// button {
/// onclick: move |_| chat_client.send(Action::Start),
/// "Start Chat Service"
/// }
/// })
/// ```
pub fn use_coroutine<M, G, F>(cx: &ScopeState, init: G) -> &CoroutineHandle<M>
where
M: 'static,
G: FnOnce(UnboundedReceiver<M>) -> F,
F: Future<Output = ()> + 'static,
{
let state = cx.use_hook(move |_| {
let f = create_future();
let id = cx.push_future(f);
State {
running: Default::default(),
_id: id
// pending_fut: Default::default(),
// running_fut: Default::default(),
}
});
cx.use_hook(|_| {
let (tx, rx) = futures_channel::mpsc::unbounded();
let task = cx.push_future(init(rx));
cx.provide_context(CoroutineHandle { tx, task })
})
}

// state.pending_fut.set(Some(Box::pin(f)));
/// Get a handle to a coroutine higher in the tree
///
/// See the docs for [`use_coroutine`] for more details.
pub fn use_coroutine_handle<M: 'static>(cx: &ScopeState) -> Option<&Rc<CoroutineHandle<M>>> {
cx.use_hook(|_| cx.consume_context::<CoroutineHandle<M>>())
.as_ref()
}

// if let Some(fut) = state.running_fut.as_mut() {
// cx.push_future(fut);
// }
pub struct CoroutineHandle<T> {
tx: UnboundedSender<T>,
task: TaskId,
}

// if let Some(fut) = state.running_fut.take() {
// state.running.set(true);
// fut.resume();
// }
impl<T> CoroutineHandle<T> {
/// Get the ID of this coroutine
#[must_use]
pub fn task_id(&self) -> TaskId {
self.task
}

// let submit: Box<dyn FnOnce() + 'a> = Box::new(move || {
// let g = async move {
// running.set(true);
// create_future().await;
// running.set(false);
// };
// let p: Pin<Box<dyn Future<Output = ()>>> = Box::pin(g);
// fut_slot
// .borrow_mut()
// .replace(unsafe { std::mem::transmute(p) });
// });
/// Send a message to the coroutine
pub fn send(&self, msg: T) {
let _ = self.tx.unbounded_send(msg);
}
}

// let submit = unsafe { std::mem::transmute(submit) };
// state.submit.get_mut().replace(submit);
#[cfg(test)]
mod tests {
#![allow(unused)]

// if state.running.get() {
// // let mut fut = state.fut.borrow_mut();
// // cx.push_task(|| fut.as_mut().unwrap().as_mut());
// } else {
// // make sure to drop the old future
// if let Some(fut) = state.fut.borrow_mut().take() {
// drop(fut);
// }
// }
CoroutineHandle { cx, inner: state }
}
use super::*;
use dioxus_core::exports::futures_channel::mpsc::unbounded;
use dioxus_core::prelude::*;
use futures_util::StreamExt;

struct State {
running: Rc<Cell<bool>>,
_id: TaskId,
// the way this is structure, you can toggle the coroutine without re-rendering the comppnent
// this means every render *generates* the future, which is a bit of a waste
// todo: allocate pending futures in the bump allocator and then have a true promotion
// pending_fut: Cell<Option<Pin<Box<dyn Future<Output = ()> + 'static>>>>,
// running_fut: Option<Pin<Box<dyn Future<Output = ()> + 'static>>>,
// running_fut: Rc<RefCell<Option<Pin<Box<dyn Future<Output = ()> + 'static>>>>>
}
fn app(cx: Scope, name: String) -> Element {
let task = use_coroutine(&cx, |mut rx: UnboundedReceiver<i32>| async move {
while let Some(msg) = rx.next().await {
println!("got message: {}", msg);
}
});

pub struct CoroutineHandle<'a> {
cx: &'a ScopeState,
inner: &'a State,
}
let task2 = use_coroutine(&cx, view_task);

impl Clone for CoroutineHandle<'_> {
fn clone(&self) -> Self {
CoroutineHandle {
cx: self.cx,
inner: self.inner,
}
let task3 = use_coroutine(&cx, |rx| complex_task(rx, 10));

None
}
}
impl Copy for CoroutineHandle<'_> {}

impl<'a> CoroutineHandle<'a> {
#[allow(clippy::needless_return)]
pub fn start(&self) {
if self.is_running() {
return;
async fn view_task(mut rx: UnboundedReceiver<i32>) {
while let Some(msg) = rx.next().await {
println!("got message: {}", msg);
}

// if let Some(submit) = self.inner.pending_fut.take() {
// submit();
// let inner = self.inner;
// self.cx.push_task(submit());
// }
}

pub fn is_running(&self) -> bool {
self.inner.running.get()
enum Actions {
CloseAll,
OpenAll,
}

pub fn resume(&self) {
// self.cx.push_task(fut)
async fn complex_task(mut rx: UnboundedReceiver<Actions>, name: i32) {
while let Some(msg) = rx.next().await {
match msg {
Actions::CloseAll => todo!(),
Actions::OpenAll => todo!(),
}
}
}

pub fn stop(&self) {}

pub fn restart(&self) {}
}
Loading