diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index f49cb8f93..34868b924 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -469,7 +469,7 @@ impl Clone for Callback { } } -impl PartialEq for Callback { +impl PartialEq for Callback { fn eq(&self, other: &Self) -> bool { self.callback.ptr_eq(&other.callback) && self.origin == other.origin } diff --git a/packages/hooks/src/use_action.rs b/packages/hooks/src/use_action.rs index 2588db99a..7a211cc78 100644 --- a/packages/hooks/src/use_action.rs +++ b/packages/hooks/src/use_action.rs @@ -5,6 +5,68 @@ use futures_channel::oneshot::Receiver; use futures_util::{future::Shared, FutureExt}; use std::{marker::PhantomData, pin::Pin, prelude::rust_2024::Future, task::Poll}; +/// Create an action that runs async work on demand, triggered by user interaction. +/// +/// Unlike [`use_resource`](crate::use_resource()) which runs automatically when its reactive +/// dependencies change, `use_action` only runs when you explicitly call it. This makes it +/// the right choice for mutations, form submissions, button clicks, and any async work that +/// should happen in response to a user event rather than on mount or state change. +/// +/// The closure you pass must return a `Future` whose output is `Result`. The action +/// tracks the lifecycle for you: pending, ready, or errored. +/// +/// ## Basic usage +/// +/// Pass a server function (or any async closure) directly: +/// +/// ```rust, ignore +/// let mut save = use_action(save_to_database); +/// +/// rsx! { +/// button { onclick: move |_| save.call(form_data.clone()), "Save" } +/// +/// if save.pending() { +/// p { "Saving..." } +/// } +/// +/// if let Some(result) = save.value() { +/// match result { +/// Ok(data) => rsx! { p { "Saved: {data}" } }, +/// Err(err) => rsx! { p { "Error: {err}" } }, +/// } +/// } +/// } +/// ``` +/// +/// # With inline async closures +/// +/// ```rust, ignore +/// let mut fetch_dog = use_action(move |breed: String| async move { +/// reqwest::get(format!("https://dog.ceo/api/breed/{breed}/images/random")) +/// .await? +/// .json::() +/// .await +/// }); +/// ``` +/// +/// ## Automatic cancellation +/// +/// Calling an action while a previous call is still pending automatically cancels the +/// in-flight task. Only the most recent call's result is kept. You can also cancel or +/// reset manually: +/// +/// ```rust, ignore +/// save.cancel(); // cancel in-flight work, reset state +/// save.reset(); // same — cancel and clear the value +/// ``` +/// +/// ## When to use `use_action` vs `use_resource` +/// +/// | | `use_action` | `use_resource` | +/// |---|---|---| +/// | **Runs** | When you call it | Automatically on mount / dependency change | +/// | **Good for** | Mutations, form submits, button clicks | Loading data to display | +/// | **Cancellation** | Auto-cancels previous call | Restarts on dependency change | pub fn use_action(mut user_fn: C) -> Action where E: Into + 'static, @@ -71,6 +133,11 @@ where } } +/// A handle to an async action created by [`use_action`]. +/// +/// Call it with `.call(...)` to dispatch work, read results with `.value()`, +/// and check progress with `.pending()`. Implements `Copy` so it can be moved +/// into multiple event handlers freely. pub struct Action { reader: ReadSignal, error: Signal>, @@ -81,7 +148,25 @@ pub struct Action { _phantom: PhantomData<*const I>, } +/// The internal state of an action +/// +/// We can never reset the state to Unset, only to Reset, otherwise the value reader would panic. +#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] +enum ActionState { + Unset, + Pending, + Ready, + Errored, + Reset, +} + impl Action { + /// The result of the most recent call, if it has completed. + /// + /// Returns `None` while the action is pending or has never been called. + /// Returns `Some(Ok(signal))` on success or `Some(Err(e))` on failure. + /// The returned `ReadSignal` is reactive — reading it in RSX will + /// subscribe the component to updates. pub fn value(&self) -> Option, CapturedError>> { if !matches!( *self.state.read(), @@ -101,11 +186,12 @@ impl Action { Some(Ok(self.reader)) } + /// Returns `true` while a call is in flight. pub fn pending(&self) -> bool { *self.state.read() == ActionState::Pending } - /// Clear the current value and error, setting the state to Reset + /// Clear the result and cancel any in-flight work. pub fn reset(&mut self) { self.state.set(ActionState::Reset); if let Some(t) = self.task.take() { @@ -113,6 +199,7 @@ impl Action { } } + /// Cancel the in-flight task without clearing the previous result's state. pub fn cancel(&mut self) { if let Some(t) = self.task.take() { t.cancel() @@ -137,6 +224,13 @@ where } } } + +impl PartialEq for Action { + fn eq(&self, other: &Self) -> bool { + self.callback == other.callback + } +} + pub struct Dispatching { _phantom: PhantomData<*const I>, receiver: Shared>, @@ -169,110 +263,74 @@ impl Clone for Action { } } -/// The state of an action -/// -/// We can never reset the state to Unset, only to Reset, otherwise the value reader would panic. -#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)] -enum ActionState { - Unset, - Pending, - Ready, - Errored, - Reset, -} - -pub trait ActionCallback { +pub trait ActionCallback { type Input; type Output; fn call( &mut self, input: Self::Input, - ) -> impl Future> + 'static; + ) -> impl Future> + 'static; } -impl ActionCallback<(O,), E> for F -where - F: FnMut() -> G, - G: Future> + 'static, -{ - type Input = (); - type Output = O; - fn call( - &mut self, - _input: Self::Input, - ) -> impl Future> + 'static { - (self)() - } +macro_rules! impl_action_callback { + // Base case: zero args + () => { + impl ActionCallback<(Out,), Err> for Func + where + Func: FnMut() -> Fut, + Fut: Future> + 'static, + { + type Input = (); + type Output = Out; + fn call( + &mut self, + _input: Self::Input, + ) -> impl Future> + 'static { + (self)() + } + } + + impl Action<(), Out> { + /// Dispatch the action with no arguments. + pub fn call(&mut self) -> Dispatching<()> { + Dispatching::new((self.callback).call(())) + } + } + }; + + // N-arg case + ($($arg:ident),+) => { + impl ActionCallback<($($arg,)+ Out), Err> for Func + where + Func: FnMut($($arg),+) -> Fut, + Fut: Future> + 'static, + { + type Input = ($($arg,)+); + type Output = Out; + fn call( + &mut self, + input: Self::Input, + ) -> impl Future> + 'static { + #[allow(non_snake_case)] + let ($($arg,)+) = input; + (self)($($arg),+) + } + } + + impl<$($arg: 'static,)+ Out> Action<($($arg,)+), Out> { + /// Dispatch the action with the given arguments. + #[allow(non_snake_case)] + pub fn call(&mut self, $($arg: $arg),+) -> Dispatching<()> { + Dispatching::new((self.callback).call(($($arg,)+))) + } + } + }; } -impl ActionCallback<(A, O), E> for F -where - F: FnMut(A) -> G, - G: Future> + 'static, -{ - type Input = (A,); - type Output = O; - fn call( - &mut self, - input: Self::Input, - ) -> impl Future> + 'static { - let (a,) = input; - (self)(a) - } -} - -impl ActionCallback<(A, B, O), E> for F -where - F: FnMut(A, B) -> G, - G: Future> + 'static, -{ - type Input = (A, B); - type Output = O; - fn call( - &mut self, - input: Self::Input, - ) -> impl Future> + 'static { - let (a, b) = input; - (self)(a, b) - } -} - -impl ActionCallback<(A, B, C, O), E> for F -where - F: FnMut(A, B, C) -> G, - G: Future> + 'static, -{ - type Input = (A, B, C); - type Output = O; - fn call( - &mut self, - input: Self::Input, - ) -> impl Future> + 'static { - let (a, b, c) = input; - (self)(a, b, c) - } -} - -impl Action<(), O> { - pub fn call(&mut self) -> Dispatching<()> { - Dispatching::new((self.callback).call(())) - } -} - -impl Action<(A,), O> { - pub fn call(&mut self, _a: A) -> Dispatching<()> { - Dispatching::new((self.callback).call((_a,))) - } -} - -impl Action<(A, B), O> { - pub fn call(&mut self, _a: A, _b: B) -> Dispatching<()> { - Dispatching::new((self.callback).call((_a, _b))) - } -} - -impl Action<(A, B, C), O> { - pub fn call(&mut self, _a: A, _b: B, _c: C) -> Dispatching<()> { - Dispatching::new((self.callback).call((_a, _b, _c))) - } -} +impl_action_callback!(); +impl_action_callback!(A); +impl_action_callback!(A, B); +impl_action_callback!(A, B, C); +impl_action_callback!(A, B, C, D); +impl_action_callback!(A, B, C, D, E); +impl_action_callback!(A, B, C, D, E, F);