Skip to main content

dioxus_hooks/
use_action.rs

1use crate::{use_callback, use_signal};
2use dioxus_core::{use_hook, Callback, CapturedError, Result, Task};
3use dioxus_signals::{ReadSignal, ReadableBoxExt, ReadableExt, Signal, WritableExt};
4use futures_channel::oneshot::Receiver;
5use futures_util::{future::Shared, FutureExt};
6use std::{marker::PhantomData, pin::Pin, prelude::rust_2024::Future, task::Poll};
7
8/// Create an action that runs async work on demand, triggered by user interaction.
9///
10/// Unlike [`use_resource`](crate::use_resource()) which runs automatically when its reactive
11/// dependencies change, `use_action` only runs when you explicitly call it. This makes it
12/// the right choice for mutations, form submissions, button clicks, and any async work that
13/// should happen in response to a user event rather than on mount or state change.
14///
15/// The closure you pass must return a `Future` whose output is `Result<T, E>`. The action
16/// tracks the lifecycle for you: pending, ready, or errored.
17///
18/// ## Basic usage
19///
20/// Pass a server function (or any async closure) directly:
21///
22/// ```rust, ignore
23/// let mut save = use_action(save_to_database);
24///
25/// rsx! {
26///     button { onclick: move |_| save.call(form_data.clone()), "Save" }
27///
28///     if save.pending() {
29///         p { "Saving..." }
30///     }
31///
32///     if let Some(result) = save.value() {
33///         match result {
34///             Ok(data) => rsx! { p { "Saved: {data}" } },
35///             Err(err) => rsx! { p { "Error: {err}" } },
36///         }
37///     }
38/// }
39/// ```
40///
41/// # With inline async closures
42///
43/// ```rust, ignore
44/// let mut fetch_dog = use_action(move |breed: String| async move {
45///     reqwest::get(format!("https://dog.ceo/api/breed/{breed}/images/random"))
46///         .await?
47///         .json::<DogImage>()
48///         .await
49/// });
50/// ```
51///
52/// ## Automatic cancellation
53///
54/// Calling an action while a previous call is still pending automatically cancels the
55/// in-flight task. Only the most recent call's result is kept. You can also cancel or
56/// reset manually:
57///
58/// ```rust, ignore
59/// save.cancel(); // cancel in-flight work, reset state
60/// save.reset();  // same — cancel and clear the value
61/// ```
62///
63/// ## When to use `use_action` vs `use_resource`
64///
65/// | | `use_action` | `use_resource` |
66/// |---|---|---|
67/// | **Runs** | When you call it | Automatically on mount / dependency change |
68/// | **Good for** | Mutations, form submits, button clicks | Loading data to display |
69/// | **Cancellation** | Auto-cancels previous call | Restarts on dependency change |
70pub fn use_action<E, C, M>(mut user_fn: C) -> Action<C::Input, C::Output>
71where
72    E: Into<CapturedError> + 'static,
73    C: ActionCallback<M, E>,
74    M: 'static,
75    C::Input: 'static,
76    C::Output: 'static,
77    C: 'static,
78{
79    let mut value = use_signal(|| None as Option<C::Output>);
80    let mut error = use_signal(|| None as Option<CapturedError>);
81    let mut task = use_signal(|| None as Option<Task>);
82    let mut state = use_signal(|| ActionState::Unset);
83    let callback = use_callback(move |input: C::Input| {
84        // Cancel any existing task
85        if let Some(task) = task.take() {
86            task.cancel();
87        }
88
89        let (tx, rx) = futures_channel::oneshot::channel();
90        let rx = rx.shared();
91
92        // Spawn a new task, and *then* fire off the async
93        let result = user_fn.call(input);
94        let new_task = dioxus_core::spawn(async move {
95            // Set the state to pending
96            state.set(ActionState::Pending);
97
98            // Create a new task
99            let result = result.await;
100            match result {
101                Ok(res) => {
102                    error.set(None);
103                    value.set(Some(res));
104                    state.set(ActionState::Ready);
105                }
106                Err(err) => {
107                    error.set(Some(err.into()));
108                    value.set(None);
109                    state.set(ActionState::Errored);
110                }
111            }
112
113            tx.send(()).ok();
114        });
115
116        task.set(Some(new_task));
117
118        rx
119    });
120
121    // Create a reader that maps the Option<T> to T, unwrapping the Option
122    // This should only be handed out if we know the value is Some. We never set the value back to None, only modify the state of the action
123    let reader = use_hook(|| value.boxed().map(|v| v.as_ref().unwrap()).boxed());
124
125    Action {
126        value,
127        error,
128        task,
129        callback,
130        reader,
131        _phantom: PhantomData,
132        state,
133    }
134}
135
136/// A handle to an async action created by [`use_action`].
137///
138/// Call it with `.call(...)` to dispatch work, read results with `.value()`,
139/// and check progress with `.pending()`. Implements `Copy` so it can be moved
140/// into multiple event handlers freely.
141pub struct Action<I, T: 'static> {
142    reader: ReadSignal<T>,
143    error: Signal<Option<CapturedError>>,
144    value: Signal<Option<T>>,
145    task: Signal<Option<Task>>,
146    callback: Callback<I, Shared<Receiver<()>>>,
147    state: Signal<ActionState>,
148    _phantom: PhantomData<*const I>,
149}
150
151/// The internal state of an action
152///
153/// We can never reset the state to Unset, only to Reset, otherwise the value reader would panic.
154#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
155enum ActionState {
156    Unset,
157    Pending,
158    Ready,
159    Errored,
160    Reset,
161}
162
163impl<I: 'static, O: 'static> Action<I, O> {
164    /// The result of the most recent call, if it has completed.
165    ///
166    /// Returns `None` while the action is pending or has never been called.
167    /// Returns `Some(Ok(signal))` on success or `Some(Err(e))` on failure.
168    /// The returned `ReadSignal` is reactive — reading it in RSX will
169    /// subscribe the component to updates.
170    pub fn value(&self) -> Option<Result<ReadSignal<O>, CapturedError>> {
171        if !matches!(
172            *self.state.read(),
173            ActionState::Ready | ActionState::Errored
174        ) {
175            return None;
176        }
177
178        if let Some(err) = self.error.cloned() {
179            return Some(Err(err));
180        }
181
182        if self.value.read().is_none() {
183            return None;
184        }
185
186        Some(Ok(self.reader))
187    }
188
189    /// Returns `true` while a call is in flight.
190    pub fn pending(&self) -> bool {
191        *self.state.read() == ActionState::Pending
192    }
193
194    /// Clear the result and cancel any in-flight work.
195    pub fn reset(&mut self) {
196        self.state.set(ActionState::Reset);
197        if let Some(t) = self.task.take() {
198            t.cancel()
199        }
200    }
201
202    /// Cancel the in-flight task without clearing the previous result's state.
203    pub fn cancel(&mut self) {
204        if let Some(t) = self.task.take() {
205            t.cancel()
206        }
207        self.state.set(ActionState::Reset);
208    }
209}
210
211impl<I, T> std::fmt::Debug for Action<I, T>
212where
213    T: std::fmt::Debug + 'static,
214{
215    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
216        if f.alternate() {
217            f.debug_struct("Action")
218                .field("state", &self.state.read())
219                .field("value", &self.value.read())
220                .field("error", &self.error.read())
221                .finish()
222        } else {
223            std::fmt::Debug::fmt(&self.value.read().as_ref(), f)
224        }
225    }
226}
227
228impl<I, T> PartialEq for Action<I, T> {
229    fn eq(&self, other: &Self) -> bool {
230        self.callback == other.callback
231    }
232}
233
234pub struct Dispatching<I> {
235    _phantom: PhantomData<*const I>,
236    receiver: Shared<Receiver<()>>,
237}
238
239impl<T> Dispatching<T> {
240    pub(crate) fn new(receiver: Shared<Receiver<()>>) -> Self {
241        Self {
242            _phantom: PhantomData,
243            receiver,
244        }
245    }
246}
247
248impl<T> std::future::Future for Dispatching<T> {
249    type Output = ();
250
251    fn poll(mut self: Pin<&mut Self>, _cx: &mut std::task::Context<'_>) -> Poll<Self::Output> {
252        match self.receiver.poll_unpin(_cx) {
253            Poll::Ready(_) => Poll::Ready(()),
254            Poll::Pending => Poll::Pending,
255        }
256    }
257}
258
259impl<I, T> Copy for Action<I, T> {}
260impl<I, T> Clone for Action<I, T> {
261    fn clone(&self) -> Self {
262        *self
263    }
264}
265
266pub trait ActionCallback<Marker, Err> {
267    type Input;
268    type Output;
269    fn call(
270        &mut self,
271        input: Self::Input,
272    ) -> impl Future<Output = Result<Self::Output, Err>> + 'static;
273}
274
275macro_rules! impl_action_callback {
276    // Base case: zero args
277    () => {
278        impl<Func, Out, Fut, Err> ActionCallback<(Out,), Err> for Func
279        where
280            Func: FnMut() -> Fut,
281            Fut: Future<Output = Result<Out, Err>> + 'static,
282        {
283            type Input = ();
284            type Output = Out;
285            fn call(
286                &mut self,
287                _input: Self::Input,
288            ) -> impl Future<Output = Result<Self::Output, Err>> + 'static {
289                (self)()
290            }
291        }
292
293        impl<Out> Action<(), Out> {
294            /// Dispatch the action with no arguments.
295            pub fn call(&mut self) -> Dispatching<()> {
296                Dispatching::new((self.callback).call(()))
297            }
298        }
299    };
300
301    // N-arg case
302    ($($arg:ident),+) => {
303        impl<Func, Out, $($arg,)+ Fut, Err> ActionCallback<($($arg,)+ Out), Err> for Func
304        where
305            Func: FnMut($($arg),+) -> Fut,
306            Fut: Future<Output = Result<Out, Err>> + 'static,
307        {
308            type Input = ($($arg,)+);
309            type Output = Out;
310            fn call(
311                &mut self,
312                input: Self::Input,
313            ) -> impl Future<Output = Result<Self::Output, Err>> + 'static {
314                #[allow(non_snake_case)]
315                let ($($arg,)+) = input;
316                (self)($($arg),+)
317            }
318        }
319
320        impl<$($arg: 'static,)+ Out> Action<($($arg,)+), Out> {
321            /// Dispatch the action with the given arguments.
322            #[allow(non_snake_case)]
323            pub fn call(&mut self, $($arg: $arg),+) -> Dispatching<()> {
324                Dispatching::new((self.callback).call(($($arg,)+)))
325            }
326        }
327    };
328}
329
330impl_action_callback!();
331impl_action_callback!(A);
332impl_action_callback!(A, B);
333impl_action_callback!(A, B, C);
334impl_action_callback!(A, B, C, D);
335impl_action_callback!(A, B, C, D, E);
336impl_action_callback!(A, B, C, D, E, F);