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}