euv_core/reactive/hook/fn.rs
1use crate::*;
2
3/// Returns the currently active `HookContext`.
4///
5/// If no hook context has been set, creates and stores a default one
6/// in the global `CURRENT_HOOK_CONTEXT` cell so subsequent calls
7/// return the same instance.
8///
9/// # Returns
10///
11/// - `HookContext` - The currently active hook context.
12pub(crate) fn get_current_hook_context() -> HookContext {
13 match current_hook_context() {
14 Some(hook_context_rc) => HookContext::new(hook_context_rc.clone()),
15 None => {
16 let rc: HookContextRc = Rc::new(RefCell::new(HookContextInner::default()));
17 *current_hook_context_mut() = Some(rc.clone());
18 HookContext::new(rc)
19 }
20 }
21}
22
23/// Runs a closure with the given `HookContext` set as the active context.
24///
25/// Saves the previous context, sets the new one, executes the closure,
26/// and restores the previous context afterward.
27///
28/// # Arguments
29///
30/// - `HookContext` - The hook context to set as active during closure execution.
31/// - `FnOnce() -> R` - The closure to execute with the given context.
32///
33/// # Returns
34///
35/// - `R` - The result of the closure execution.
36pub(crate) fn with_hook_context<F, R>(context: HookContext, f: F) -> R
37where
38 F: FnOnce() -> R,
39{
40 let previous: Option<HookContextRc> = current_hook_context_mut().take();
41 *current_hook_context_mut() = Some(context.get_inner().clone());
42 let result: R = f();
43 *current_hook_context_mut() = previous;
44 result
45}
46
47/// Creates a new `HookContext` with a fresh inner state.
48///
49/// Delegates to `HookContext::default()` which initializes an empty
50/// hook storage list, a zero hook index, and an empty cleanup list.
51///
52/// # Returns
53///
54/// - `HookContext` - A newly created hook context with default state.
55pub(crate) fn create_hook_context() -> HookContext {
56 HookContext::default()
57}
58
59/// Creates a new reactive signal with the given initial value.
60///
61/// Uses the current `HookContext` to maintain signal identity across
62/// re-renders. On the first call at a given hook index, the signal
63/// is created with `init()` and stored. On subsequent re-renders,
64/// the existing signal at that index is returned unchanged.
65///
66/// # Arguments
67///
68/// - `FnOnce() -> T` - A closure that computes the initial value of the signal.
69///
70/// # Returns
71///
72/// - `Signal<T>` - A reactive signal containing the initialized or existing value.
73pub fn use_signal<T, F>(init: F) -> Signal<T>
74where
75 T: Clone + PartialEq + 'static,
76 F: FnOnce() -> T,
77{
78 let hook_context: HookContext = get_current_hook_context();
79 let Ok(mut inner) = hook_context.get_inner().try_borrow_mut() else {
80 return Signal::create(init());
81 };
82 let index: usize = inner.get_hook_index();
83 inner.set_hook_index(index + 1);
84 if index < inner.get_hooks().len()
85 && let Some(existing) = inner.get_hooks()[index].downcast_ref::<Signal<T>>()
86 {
87 return *existing;
88 }
89 let signal: Signal<T> = Signal::create(init());
90 inner
91 .get_mut_cleanups()
92 .push(Box::new(move || signal.clear_listeners()));
93 if index < inner.get_hooks().len() {
94 inner.get_mut_hooks()[index] = Box::new(signal);
95 } else {
96 inner.get_mut_hooks().push(Box::new(signal));
97 }
98 signal
99}
100
101/// Registers a cleanup callback that will be executed when the current
102/// hook context is cleared (e.g., when a `match` arm switches).
103///
104/// This is useful for cleaning up side effects like intervals, timeouts,
105/// or subscriptions that are not automatically managed by signals.
106///
107/// The cleanup callback is only registered once on the first render.
108/// On subsequent re-renders at the same hook index, this is a no-op.
109///
110/// # Arguments
111///
112/// - `FnOnce() + 'static` - The cleanup callback to execute on context teardown.
113pub fn use_cleanup<F>(cleanup: F)
114where
115 F: FnOnce() + 'static,
116{
117 let hook_context: HookContext = get_current_hook_context();
118 let Ok(mut inner) = hook_context.get_inner().try_borrow_mut() else {
119 return;
120 };
121 let index: usize = inner.get_hook_index();
122 inner.set_hook_index(index + 1);
123 if index < inner.get_hooks().len() {
124 return;
125 }
126 inner.get_mut_cleanups().push(Box::new(cleanup));
127 inner.get_mut_hooks().push(Box::new(()));
128}
129
130/// Registers a `window.addEventListener` callback using event delegation,
131/// automatically removed when the hook context is cleared.
132///
133/// Uses the global window event proxy registry so that only one
134/// `window.addEventListener` call is made per event name regardless of
135/// how many components listen to the same event. On cleanup, only the
136/// handler entry is removed from the proxy registry; the shared window
137/// listener remains active for other consumers.
138///
139/// The event listener is only registered once on the first render.
140/// On subsequent re-renders at the same hook index, this is a no-op.
141///
142/// # Arguments
143///
144/// - `&str` - The event name to listen for (e.g., "hashchange", "popstate", "resize").
145/// - `FnMut() + 'static` - The callback to invoke when the event fires.
146pub fn use_window_event<F>(event_name: &str, callback: F)
147where
148 F: FnMut() + 'static,
149{
150 let hook_context: HookContext = get_current_hook_context();
151 let Ok(mut inner) = hook_context.get_inner().try_borrow_mut() else {
152 return;
153 };
154 let index: usize = inner.get_hook_index();
155 inner.set_hook_index(index + 1);
156 if index < inner.get_hooks().len() {
157 return;
158 }
159 let event_name_owned: String = event_name.to_string();
160 let handler_id: usize = register_window_event_handler(event_name, callback);
161 inner.get_mut_cleanups().push(Box::new(move || {
162 unregister_window_event_handler(&event_name_owned, handler_id);
163 }));
164 inner.get_mut_hooks().push(Box::new(()));
165}
166
167/// Creates a recurring interval that invokes the given closure at the
168/// specified period, returning an `IntervalHandle` that is automatically
169/// cleared when the hook context is cleared (i.e., when the component
170/// unmounts or a `match` arm switches).
171///
172/// Unlike calling `set_interval_with_callback_and_timeout_and_arguments_0`
173/// + `Closure::forget()` manually, this hook ensures the interval is
174/// properly cleaned up, preventing memory leaks and stale callbacks.
175///
176/// The interval is only created once on the first render.
177/// On subsequent re-renders at the same hook index, the existing handle
178/// is returned unchanged.
179///
180/// # Arguments
181///
182/// - `i32` - The interval period in milliseconds.
183/// - `FnMut() + 'static` - The closure to invoke on each interval tick.
184///
185/// # Returns
186///
187/// - `IntervalHandle` - A handle that can be used to cancel the interval early.
188///
189/// # Panics
190///
191/// Panics if `window()` is unavailable on the current platform.
192pub fn use_interval<F>(millis: i32, callback: F) -> IntervalHandle
193where
194 F: FnMut() + 'static,
195{
196 let hook_context: HookContext = get_current_hook_context();
197 let Ok(mut inner) = hook_context.get_inner().try_borrow_mut() else {
198 return IntervalHandle::new(0);
199 };
200 let index: usize = inner.get_hook_index();
201 inner.set_hook_index(index + 1);
202 if index < inner.get_hooks().len()
203 && let Some(existing) = inner.get_hooks()[index].downcast_ref::<IntervalHandle>()
204 {
205 return *existing;
206 }
207 let closure: Closure<dyn FnMut()> = Closure::wrap(Box::new(callback));
208 let window: Window = window().expect("no global window exists");
209 let interval_id: i32 = window
210 .set_interval_with_callback_and_timeout_and_arguments_0(
211 closure.as_ref().unchecked_ref(),
212 millis,
213 )
214 .expect("failed to set interval");
215 closure.forget();
216 let handle: IntervalHandle = IntervalHandle::new(interval_id);
217 inner.get_mut_cleanups().push(Box::new(move || {
218 let Some(cleanup_window) = web_sys::window() else {
219 return;
220 };
221 cleanup_window.clear_interval_with_handle(interval_id);
222 }));
223 if index < inner.get_hooks().len() {
224 inner.get_mut_hooks()[index] = Box::new(handle);
225 } else {
226 inner.get_mut_hooks().push(Box::new(handle));
227 }
228 handle
229}