diff --git a/packages/yew/Cargo.toml b/packages/yew/Cargo.toml index dddf528acbc..07c095f9200 100644 --- a/packages/yew/Cargo.toml +++ b/packages/yew/Cargo.toml @@ -73,6 +73,7 @@ tokio = { version = "1.15.0", features = ["rt"], optional = true } [dev-dependencies] wasm-bindgen-test = "0.3" gloo = { version = "0.7", features = ["futures"] } +gloo-net = { version = "0.2", features = ["http"] } wasm-bindgen-futures = "0.4" rustversion = "1" trybuild = "1" diff --git a/packages/yew/Makefile.toml b/packages/yew/Makefile.toml index c08f61944a9..0e0f83bba0b 100644 --- a/packages/yew/Makefile.toml +++ b/packages/yew/Makefile.toml @@ -25,7 +25,7 @@ args = [ "test", "--doc", "--features", - "doc_test,wasm_test", + "tokio,doc_test,wasm_test", ] [tasks.ssr-test] diff --git a/packages/yew/src/functional/hooks/use_memo.rs b/packages/yew/src/functional/hooks/use_memo.rs index a97ca2421da..7574f6a673e 100644 --- a/packages/yew/src/functional/hooks/use_memo.rs +++ b/packages/yew/src/functional/hooks/use_memo.rs @@ -1,7 +1,42 @@ -use std::cell::RefCell; -use std::rc::Rc; +use super::use_mut_ref; +use std::{borrow::Borrow, rc::Rc}; -use crate::functional::{hook, use_state}; +use crate::functional::hook; + +/// Get a immutable reference to a memoized value. +/// +/// This version allows for a key cache key derivation that only borrows +/// like the original argument. For example, using ` K = Rc`, we only +/// create a shared reference to dependencies *after* they change. +#[hook] +pub(crate) fn use_memo_base(f: F, deps: D) -> Rc +where + T: 'static, + F: FnOnce(D) -> (T, K), + K: 'static + Borrow, + D: PartialEq, +{ + struct MemoState { + memo_key: K, + result: Rc, + } + let state = use_mut_ref(|| -> Option> { None }); + + let mut state = state.borrow_mut(); + match &*state { + Some(existing) if existing.memo_key.borrow() != &deps => { + // Drop old state if it's outdated + *state = None; + } + _ => {} + }; + let state = state.get_or_insert_with(|| { + let (result, memo_key) = f(deps); + let result = Rc::new(result); + MemoState { result, memo_key } + }); + state.result.clone() +} /// Get a immutable reference to a memoized value. /// @@ -39,25 +74,5 @@ where F: FnOnce(&D) -> T, D: 'static + PartialEq, { - let val = use_state(|| -> RefCell>> { RefCell::new(None) }); - let last_deps = use_state(|| -> RefCell> { RefCell::new(None) }); - - let mut val = val.borrow_mut(); - let mut last_deps = last_deps.borrow_mut(); - - match ( - val.as_ref(), - last_deps.as_ref().and_then(|m| (m != &deps).then(|| ())), - ) { - // Previous value exists and last_deps == deps - (Some(m), None) => m.clone(), - _ => { - let new_val = Rc::new(f(&deps)); - *last_deps = Some(deps); - - *val = Some(new_val.clone()); - - new_val - } - } + use_memo_base(|d| (f(&d), d), deps) } diff --git a/packages/yew/src/functional/hooks/use_ref.rs b/packages/yew/src/functional/hooks/use_ref.rs index 045b9867d50..03aa555024f 100644 --- a/packages/yew/src/functional/hooks/use_ref.rs +++ b/packages/yew/src/functional/hooks/use_ref.rs @@ -1,9 +1,21 @@ use std::cell::RefCell; use std::rc::Rc; -use crate::functional::{hook, use_memo, use_state}; +use crate::functional::{hook, use_state, Hook, HookContext}; use crate::NodeRef; +struct UseMutRef { + init_fn: F, +} + +impl T> Hook for UseMutRef { + type Output = Rc>; + + fn run(self, ctx: &mut HookContext) -> Self::Output { + ctx.next_state(|_| RefCell::new((self.init_fn)())) + } +} + /// This hook is used for obtaining a mutable reference to a stateful value. /// Its state persists across renders. /// @@ -50,12 +62,11 @@ use crate::NodeRef; /// } /// } /// ``` -#[hook] -pub fn use_mut_ref(init_fn: F) -> Rc> +pub fn use_mut_ref(init_fn: F) -> impl Hook>> where F: FnOnce() -> T, { - use_memo(|_| RefCell::new(init_fn()), ()) + UseMutRef { init_fn } } /// This hook is used for obtaining a [`NodeRef`]. diff --git a/packages/yew/src/suspense/hooks.rs b/packages/yew/src/suspense/hooks.rs index 2240a0e21f3..73f62dd4fac 100644 --- a/packages/yew/src/suspense/hooks.rs +++ b/packages/yew/src/suspense/hooks.rs @@ -1,9 +1,11 @@ #[cfg_attr(documenting, doc(cfg(any(target_arch = "wasm32", feature = "tokio"))))] #[cfg(any(target_arch = "wasm32", feature = "tokio"))] mod feat_futures { + use std::cell::Cell; use std::fmt; use std::future::Future; use std::ops::Deref; + use std::rc::Rc; use yew::prelude::*; use yew::suspense::{Suspension, SuspensionResult}; @@ -32,25 +34,90 @@ mod feat_futures { } } + /// Use the result of an async computation, suspending while waiting. + /// + /// Awaits the future returned from the first call to `init_f`, and returns + /// its result in a [`UseFutureHandle`]. Always suspends initially, even if + /// the future is immediately [ready]. + /// + /// [ready]: std::task::Poll::Ready + /// + /// # Example + /// + /// ``` + /// # use yew::prelude::*; + /// # use yew::suspense::use_future; + /// use gloo_net::http::Request; + /// + /// const URL: &str = "https://en.wikipedia.org/w/api.php?\ + /// action=query&origin=*&format=json&generator=search&\ + /// gsrnamespace=0&gsrlimit=5&gsrsearch='New_England_Patriots'"; + /// + /// #[function_component] + /// fn WikipediaSearch() -> HtmlResult { + /// let res = use_future(|| async { Request::new(URL).send().await?.text().await })?; + /// let result_html = match *res { + /// Ok(ref res) => html! { res }, + /// Err(ref failure) => failure.to_string().into(), + /// }; + /// Ok(html! { + ///

+ /// {"Wikipedia search result: "} + /// {result_html} + ///

+ /// }) + /// } + /// ``` + #[hook] + pub fn use_future(init_f: F) -> SuspensionResult> + where + F: FnOnce() -> T, + T: Future + 'static, + O: 'static, + { + use_future_with_deps(move |_| init_f(), ()) + } + + /// Use the result of an async computation with dependencies, suspending while waiting. + /// + /// Awaits the future returned from `f` for the latest `deps`. Even if the future is immediately + /// [ready], the hook suspends at least once. If the dependencies + /// change while a future is still pending, the result is never used. This guarantees that your + /// component always sees up-to-date values while it is not suspended. + /// + /// [ready]: std::task::Poll::Ready #[hook] - pub fn use_future(f: F) -> SuspensionResult> + pub fn use_future_with_deps(f: F, deps: D) -> SuspensionResult> where - F: FnOnce() -> T + 'static, + F: FnOnce(Rc) -> T, T: Future + 'static, O: 'static, + D: PartialEq + 'static, { let output = use_state(|| None); + // We only commit a result if it comes from the latest spawned future. Otherwise, this + // might trigger pointless updates or even override newer state. + let latest_id = use_state(|| Cell::new(0u32)); let suspension = { let output = output.clone(); - use_memo( - move |_| { - Suspension::from_future(async move { - output.set(Some(f().await)); - }) + use_memo_base( + move |deps| { + let self_id = latest_id.get().wrapping_add(1); + // As long as less than 2**32 futures are in flight wrapping_add is fine + (*latest_id).set(self_id); + let deps = Rc::new(deps); + let task = f(deps.clone()); + let suspension = Suspension::from_future(async move { + let result = task.await; + if latest_id.get() == self_id { + output.set(Some(result)); + } + }); + (suspension, deps) }, - (), + deps, ) }; diff --git a/packages/yew/tests/suspense.rs b/packages/yew/tests/suspense.rs index 976f0270bf2..f996a850ec6 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::{use_future, Suspension, SuspensionResult}; +use yew::suspense::{use_future, use_future_with_deps, Suspension, SuspensionResult}; #[wasm_bindgen_test] async fn suspense_works() { @@ -634,3 +634,53 @@ async fn use_suspending_future_works() { let result = obtain_result(); assert_eq!(result.as_str(), r#"
Content
"#); } + +#[wasm_bindgen_test] +async fn use_suspending_future_with_deps_works() { + #[derive(PartialEq, Properties)] + struct ContentProps { + delay_millis: u32, + } + + #[function_component(Content)] + fn content(ContentProps { delay_millis }: &ContentProps) -> HtmlResult { + let delayed_result = use_future_with_deps( + |delay_millis| async move { + TimeoutFuture::new(*delay_millis).await; + 42 + }, + *delay_millis, + )?; + + Ok(html! { +
+ {*delayed_result} +
+ }) + } + + #[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#"
42
"#); +}