leptos_use/use_timeout_fn.rs
1use leptos::prelude::*;
2use std::marker::PhantomData;
3
4/// Wrapper for `setTimeout` with controls.
5///
6/// ## Demo
7///
8/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_timeout_fn)
9///
10/// ## Usage
11///
12/// ```
13/// # use leptos::prelude::*;
14/// # use leptos_use::{use_timeout_fn, UseTimeoutFnReturn};
15/// #
16/// # #[component]
17/// # fn Demo() -> impl IntoView {
18/// let UseTimeoutFnReturn { start, stop, is_pending, .. } = use_timeout_fn(
19/// |i: i32| {
20/// // do sth
21/// },
22/// 3000.0
23/// );
24///
25/// start(3);
26/// #
27/// # view! { }
28/// # }
29/// ```
30///
31/// ## Rescheduling
32///
33/// Calling `start` while a previous timer is still pending cancels that pending
34/// timer and schedules a fresh one. Only the most recent `start` call's
35/// callback will fire. This matches VueUse's `useTimeoutFn` semantics.
36///
37/// ## SendWrapped Return
38///
39/// The returned closures `start` and `stop` are sendwrapped functions. They can
40/// only be called from the same thread that called `use_timeout_fn`.
41///
42/// ## Server-Side Rendering
43///
44/// > Make sure you follow the [instructions in Server-Side Rendering](https://leptos-use.rs/server_side_rendering.html).
45///
46/// On the server the callback will never be run. The returned functions are all no-ops and
47/// `is_pending` will always be `false`.
48pub fn use_timeout_fn<CbFn, Arg, D>(
49 callback: CbFn,
50 delay: D,
51) -> UseTimeoutFnReturn<impl Fn(Arg) + Clone + Send + Sync, Arg, impl Fn() + Clone + Send + Sync>
52where
53 CbFn: Fn(Arg) + Clone + 'static,
54 Arg: 'static,
55 D: Into<Signal<f64>>,
56{
57 let delay = delay.into();
58
59 let (is_pending, set_pending) = signal(false);
60
61 let start;
62 let stop;
63
64 #[cfg(not(feature = "ssr"))]
65 {
66 use crate::sendwrap_fn;
67 use leptos::leptos_dom::helpers::TimeoutHandle;
68 use std::sync::{Arc, Mutex};
69 use std::time::Duration;
70
71 let timer = Arc::new(Mutex::new(None::<TimeoutHandle>));
72
73 let clear = {
74 let timer = Arc::clone(&timer);
75
76 move || {
77 let timer = timer.lock().unwrap();
78 if let Some(timer) = *timer {
79 timer.clear();
80 }
81 }
82 };
83
84 stop = {
85 let clear = clear.clone();
86
87 sendwrap_fn!(move || {
88 set_pending.set(false);
89 clear();
90 })
91 };
92
93 start = {
94 let timer = Arc::clone(&timer);
95 let callback = callback.clone();
96 let clear = clear.clone();
97
98 sendwrap_fn!(move |arg: Arg| {
99 clear();
100 set_pending.set(true);
101
102 let handle = set_timeout_with_handle(
103 {
104 let timer = Arc::clone(&timer);
105 let callback = callback.clone();
106
107 move || {
108 set_pending.set(false);
109 *timer.lock().unwrap() = None;
110
111 #[cfg(debug_assertions)]
112 let _z = leptos::reactive::diagnostics::SpecialNonReactiveZone::enter();
113
114 callback(arg);
115 }
116 },
117 Duration::from_millis(delay.get_untracked() as u64),
118 )
119 .ok();
120
121 *timer.lock().unwrap() = handle;
122 })
123 };
124
125 on_cleanup(clear);
126 }
127
128 #[cfg(feature = "ssr")]
129 {
130 let _ = set_pending;
131 let _ = callback;
132 let _ = delay;
133
134 start = move |_: Arg| ();
135 stop = move || ();
136 }
137
138 UseTimeoutFnReturn {
139 is_pending: is_pending.into(),
140 start,
141 stop,
142 _marker: PhantomData,
143 }
144}
145
146/// Return type of [`use_timeout_fn`].
147pub struct UseTimeoutFnReturn<StartFn, StartArg, StopFn>
148where
149 StartFn: Fn(StartArg) + Clone + Send + Sync,
150 StopFn: Fn() + Clone + Send + Sync,
151{
152 /// Whether the timeout is pending. When the `callback` is called this is set to `false`.
153 pub is_pending: Signal<bool>,
154
155 /// Start the timeout. The `callback` will be called after `delay` milliseconds.
156 /// If a previous timer is still pending, it is cancelled and replaced.
157 pub start: StartFn,
158
159 /// Stop the timeout. If the timeout was still pending the `callback` is not called.
160 pub stop: StopFn,
161
162 _marker: PhantomData<StartArg>,
163}