Skip to main content

dioxus_sdk_time/
timeout.rs

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