diff --git a/packages/yew/src/suspense/hooks.rs b/packages/yew/src/suspense/hooks.rs new file mode 100644 index 00000000000..2240a0e21f3 --- /dev/null +++ b/packages/yew/src/suspense/hooks.rs @@ -0,0 +1,66 @@ +#[cfg_attr(documenting, doc(cfg(any(target_arch = "wasm32", feature = "tokio"))))] +#[cfg(any(target_arch = "wasm32", feature = "tokio"))] +mod feat_futures { + use std::fmt; + use std::future::Future; + use std::ops::Deref; + + use yew::prelude::*; + use yew::suspense::{Suspension, SuspensionResult}; + + /// This hook is used to await a future in a suspending context. + /// + /// A [Suspension] is created from the passed future and the result of the future + /// is the output of the suspension. + pub struct UseFutureHandle { + inner: UseStateHandle>, + } + + impl Deref for UseFutureHandle { + type Target = O; + + fn deref(&self) -> &Self::Target { + &*self.inner.as_ref().unwrap() + } + } + + impl fmt::Debug for UseFutureHandle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("UseFutureHandle") + .field("value", &format!("{:?}", self.inner)) + .finish() + } + } + + #[hook] + pub fn use_future(f: F) -> SuspensionResult> + where + F: FnOnce() -> T + 'static, + T: Future + 'static, + O: 'static, + { + let output = use_state(|| None); + + let suspension = { + let output = output.clone(); + + use_memo( + move |_| { + Suspension::from_future(async move { + output.set(Some(f().await)); + }) + }, + (), + ) + }; + + if suspension.resumed() { + Ok(UseFutureHandle { inner: output }) + } else { + Err((*suspension).clone()) + } + } +} + +#[cfg(any(target_arch = "wasm32", feature = "tokio"))] +pub use feat_futures::*; diff --git a/packages/yew/src/suspense/mod.rs b/packages/yew/src/suspense/mod.rs index aec9ac0c93d..a377be6f16b 100644 --- a/packages/yew/src/suspense/mod.rs +++ b/packages/yew/src/suspense/mod.rs @@ -1,9 +1,11 @@ //! This module provides suspense support. mod component; +mod hooks; mod suspension; #[cfg(any(feature = "csr", feature = "ssr"))] pub(crate) use component::BaseSuspense; pub use component::Suspense; +pub use hooks::*; pub use suspension::{Suspension, SuspensionHandle, SuspensionResult}; diff --git a/packages/yew/tests/suspense.rs b/packages/yew/tests/suspense.rs index 62fe40f9e36..976f0270bf2 100644 --- a/packages/yew/tests/suspense.rs +++ b/packages/yew/tests/suspense.rs @@ -15,7 +15,7 @@ use gloo::timers::future::TimeoutFuture; use wasm_bindgen::JsCast; use wasm_bindgen_futures::spawn_local; use web_sys::{HtmlElement, HtmlTextAreaElement}; -use yew::suspense::{Suspension, SuspensionResult}; +use yew::suspense::{use_future, Suspension, SuspensionResult}; #[wasm_bindgen_test] async fn suspense_works() { @@ -593,3 +593,44 @@ async fn effects_not_run_when_suspended() { ); assert_eq!(*counter.borrow(), 4); // effects ran 4 times. } + +#[wasm_bindgen_test] +async fn use_suspending_future_works() { + #[function_component(Content)] + fn content() -> HtmlResult { + let _sleep_handle = use_future(|| async move { + TimeoutFuture::new(50).await; + })?; + + Ok(html! { +
+ {"Content"} +
+ }) + } + + #[function_component(App)] + fn app() -> Html { + let fallback = html! {
{"wait..."}
}; + + html! { +
+ + + +
+ } + } + + yew::Renderer::::with_root(gloo_utils::document().get_element_by_id("output").unwrap()) + .render(); + + TimeoutFuture::new(10).await; + let result = obtain_result(); + assert_eq!(result.as_str(), "
wait...
"); + + TimeoutFuture::new(50).await; + + let result = obtain_result(); + assert_eq!(result.as_str(), r#"
Content
"#); +}