dioxus_time/
timeout.rs

1use dioxus::{
2    dioxus_core::SpawnIfAsync,
3    prelude::{Callback, Task, spawn, use_hook},
4    signals::Signal,
5};
6use futures::{SinkExt, StreamExt, channel::mpsc};
7use std::time::Duration;
8
9/// The interface to a timeout.
10///
11/// This is used to trigger the timeout with [`UseTimeout::action`].
12///
13/// See [`use_timeout`] for more information.
14pub struct UseTimeout<Args: 'static> {
15    duration: Duration,
16    sender: Signal<mpsc::UnboundedSender<Args>>,
17}
18
19impl<Args> UseTimeout<Args> {
20    /// Trigger the timeout.
21    ///
22    /// If no arguments are desired, use the [`unit`] type.
23    /// See [`use_timeout`] for more information.
24    pub fn action(&self, args: Args) -> TimeoutHandle {
25        let mut sender = (self.sender)();
26        let duration = self.duration;
27
28        let handle = spawn(async move {
29            #[cfg(not(target_family = "wasm"))]
30            tokio::time::sleep(duration).await;
31
32            #[cfg(target_family = "wasm")]
33            gloo_timers::future::sleep(duration).await;
34
35            // If this errors then the timeout was likely dropped.
36            let _ = sender.send(args).await;
37        });
38
39        TimeoutHandle { handle }
40    }
41}
42
43impl<Args> Clone for UseTimeout<Args> {
44    fn clone(&self) -> Self {
45        *self
46    }
47}
48impl<Args> Copy for UseTimeout<Args> {}
49impl<Args> PartialEq for UseTimeout<Args> {
50    fn eq(&self, other: &Self) -> bool {
51        self.duration == other.duration && self.sender == other.sender
52    }
53}
54
55/// A handle to a pending timeout.
56///
57/// A handle to a running timeout triggered with [`UseTimeout::action`].
58/// This handle allows you to cancel the timeout from triggering with [`TimeoutHandle::cancel`]
59///
60/// See [`use_timeout`] for more information.
61#[derive(Debug, Clone, Copy, PartialEq)]
62pub struct TimeoutHandle {
63    handle: Task,
64}
65
66impl TimeoutHandle {
67    /// Cancel the timeout associated with this handle.
68    pub fn cancel(self) {
69        self.handle.cancel();
70    }
71}
72
73/// A hook to run a callback after a period of time.
74///
75/// Timeouts allow you to trigger a callback that occurs after a period of time. Unlike a debounce, a timeout will not
76/// reset it's timer when triggered again. Instead, calling a timeout while it is already running will start another instance
77/// to run the callback after the provided period.
78///
79/// This hook is similar to the web [setTimeout()](https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout) API.
80///
81/// # Examples
82///
83/// Example of using a timeout:
84/// ```rust
85/// use dioxus::prelude::*;
86/// use dioxus_time::use_timeout;
87/// use std::time::Duration;
88///
89/// #[component]
90/// fn App() -> Element {
91///     // Create a timeout for two seconds.
92///     // Once triggered, this timeout will print "timeout called" after two seconds.
93///     let timeout = use_timeout(Duration::from_secs(2), |()| println!("timeout called"));
94///     
95///     rsx! {
96///         button {
97///             onclick: move |_| {
98///                 // Trigger the timeout.
99///                 timeout.action(());
100///             },
101///             "Click!"
102///         }
103///     }
104/// }
105/// ```
106///
107/// #### Cancelling Timeouts
108/// Example of cancelling a timeout. This is the equivalent of a debounce.
109/// ```rust
110/// use dioxus::prelude::*;
111/// use dioxus_time::{use_timeout, TimeoutHandle};
112/// use std::time::Duration;
113///
114/// #[component]
115/// fn App() -> Element {
116///     let mut current_timeout: Signal<Option<TimeoutHandle>> = use_signal(|| None);
117///     let timeout = use_timeout(Duration::from_secs(2), move |()| {
118///         current_timeout.set(None);
119///         println!("timeout called");
120///     });
121///     
122///     rsx! {
123///         button {
124///             onclick: move |_| {
125///                 // Cancel any currently running timeouts.
126///                 if let Some(handle) = *current_timeout.read() {
127///                     handle.cancel();
128///                 }
129///
130///                 // Trigger the timeout.
131///                 let handle = timeout.action(());
132///                 current_timeout.set(Some(handle));
133///             },
134///             "Click!"
135///         }
136///     }
137/// }
138/// ```
139///
140/// #### Async Timeouts
141/// Timeouts can accept an async callback:
142/// ```rust
143/// use dioxus::prelude::*;
144/// use dioxus_time::use_timeout;
145/// use std::time::Duration;
146///
147/// #[component]
148/// fn App() -> Element {
149///     // Create a timeout for two seconds.
150///     // We use an async sleep to wait an even longer duration after the timeout is called.
151///     let timeout = use_timeout(Duration::from_secs(2), |()| async {
152///         println!("Timeout after two total seconds.");
153///         tokio::time::sleep(Duration::from_secs(2)).await;
154///         println!("Timeout after four total seconds.");
155///     });
156///     
157///     rsx! {
158///         button {
159///             onclick: move |_| {
160///                 // Trigger the timeout.
161///                 timeout.action(());
162///             },
163///             "Click!"
164///         }
165///     }
166/// }
167/// ```
168pub fn use_timeout<Args: 'static, MaybeAsync: SpawnIfAsync<Marker>, Marker>(
169    duration: Duration,
170    callback: impl FnMut(Args) -> MaybeAsync + 'static,
171) -> UseTimeout<Args> {
172    use_hook(|| {
173        let callback = Callback::new(callback);
174        let (sender, mut receiver) = mpsc::unbounded();
175
176        spawn(async move {
177            loop {
178                if let Some(args) = receiver.next().await {
179                    callback.call(args);
180                }
181            }
182        });
183
184        UseTimeout {
185            duration,
186            sender: Signal::new(sender),
187        }
188    })
189}