Skip to main content

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 try_get_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            *try_get_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> = try_get_current_hook_context_mut().take();
41    *try_get_current_hook_context_mut() = Some(context.get_inner().clone());
42    let result: R = f();
43    *try_get_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    // Use `deactivate` on teardown — never free the allocation here.
91    //
92    // `Signal<T>` is `Copy` (just a `usize` address), so async callbacks
93    // spawned from this component — `spawn_local` futures, `setTimeout` /
94    // `setInterval` closures, Promise continuations — may still hold copies
95    // of this signal after the hook context is torn down (e.g. a `match` arm
96    // switch or a route change). Deallocating the `SignalInner` here would
97    // turn any still-pending async callback's later `.get()` / `.set()` into
98    // a dereference of a dangling pointer (use-after-free).
99    //
100    // `deactivate` marks the signal inactive and drops its listeners /
101    // dependents but keeps the allocation alive, so stale async callbacks
102    // become safe no-ops. This mirrors the contract documented on
103    // `clear_signal_listeners_by_addr`, which the DOM cleanup path already
104    // follows for the same reason.
105    inner
106        .get_mut_cleanups()
107        .push(Box::new(move || signal.deactivate()));
108    if index < inner.get_hooks().len() {
109        inner.get_mut_hooks()[index] = Box::new(signal);
110    } else {
111        inner.get_mut_hooks().push(Box::new(signal));
112    }
113    signal
114}
115
116/// Registers a cleanup callback that will be executed when the current
117/// hook context is cleared (e.g., when a `match` arm switches).
118///
119/// This is useful for cleaning up side effects like intervals, timeouts,
120/// or subscriptions that are not automatically managed by signals.
121///
122/// The cleanup callback is only registered once on the first render.
123/// On subsequent re-renders at the same hook index, this is a no-op.
124///
125/// # Arguments
126///
127/// - `FnOnce() + 'static` - The cleanup callback to execute on context teardown.
128pub fn use_cleanup<F>(cleanup: F)
129where
130    F: FnOnce() + 'static,
131{
132    let hook_context: HookContext = get_current_hook_context();
133    let Ok(mut inner) = hook_context.get_inner().try_borrow_mut() else {
134        return;
135    };
136    let index: usize = inner.get_hook_index();
137    inner.set_hook_index(index + 1);
138    if index < inner.get_hooks().len() {
139        return;
140    }
141    inner.get_mut_cleanups().push(Box::new(cleanup));
142    inner.get_mut_hooks().push(Box::new(()));
143}
144
145/// Registers a `window.addEventListener` callback using event delegation,
146/// automatically removed when the hook context is cleared.
147///
148/// Uses the global window event proxy registry so that only one
149/// `window.addEventListener` call is made per event name regardless of
150/// how many components listen to the same event. On cleanup, only the
151/// handler entry is removed from the proxy registry; the shared window
152/// listener remains active for other consumers.
153///
154/// The event listener is only registered once on the first render.
155/// On subsequent re-renders at the same hook index, this is a no-op.
156///
157/// # Arguments
158///
159/// - `E: AsRef<str>` - The event name to listen for (e.g., "hashchange", "popstate", "resize").
160/// - `FnMut() + 'static` - The callback to invoke when the event fires.
161pub fn use_window_event<E, F>(event_name: E, callback: F)
162where
163    E: AsRef<str>,
164    F: FnMut() + 'static,
165{
166    let event_name: &str = event_name.as_ref();
167    let hook_context: HookContext = get_current_hook_context();
168    let Ok(mut inner) = hook_context.get_inner().try_borrow_mut() else {
169        return;
170    };
171    let index: usize = inner.get_hook_index();
172    inner.set_hook_index(index + 1);
173    if index < inner.get_hooks().len() {
174        return;
175    }
176    let event_name_owned: String = event_name.to_owned();
177    let handler_id: usize = register_window_event_handler(event_name, callback);
178    inner.get_mut_cleanups().push(Box::new(move || {
179        unregister_window_event_handler(&event_name_owned, handler_id);
180    }));
181    inner.get_mut_hooks().push(Box::new(()));
182}
183
184/// Creates a recurring interval that invokes the given closure at the
185/// specified period, returning an `IntervalHandle` that is automatically
186/// cleared when the hook context is cleared (i.e., when the component
187/// unmounts or a `match` arm switches).
188///
189/// Unlike calling `set_interval_with_callback_and_timeout_and_arguments_0`
190/// + `Closure::forget()` manually, this hook ensures the interval is
191///   properly cleaned up, preventing memory leaks and stale callbacks.
192///
193/// The interval is only created once on the first render.
194/// On subsequent re-renders at the same hook index, the existing handle
195/// is returned unchanged.
196///
197/// # Arguments
198///
199/// - `i32` - The interval period in milliseconds.
200/// - `FnMut() + 'static` - The closure to invoke on each interval tick.
201///
202/// # Returns
203///
204/// - `IntervalHandle` - A handle that can be used to cancel the interval early.
205///
206/// # Panics
207///
208/// Panics if `window()` is unavailable on the current platform.
209pub fn use_interval<F>(millis: i32, callback: F) -> IntervalHandle
210where
211    F: FnMut() + 'static,
212{
213    let hook_context: HookContext = get_current_hook_context();
214    let Ok(mut inner) = hook_context.get_inner().try_borrow_mut() else {
215        return IntervalHandle::new(0);
216    };
217    let index: usize = inner.get_hook_index();
218    inner.set_hook_index(index + 1);
219    if index < inner.get_hooks().len()
220        && let Some(existing) = inner.get_hooks()[index].downcast_ref::<IntervalHandle>()
221    {
222        return *existing;
223    }
224    let closure: Closure<dyn FnMut()> = Closure::wrap(Box::new(callback));
225    let window: Window = window().expect("no global window exists");
226    let interval_id: i32 = window
227        .set_interval_with_callback_and_timeout_and_arguments_0(
228            closure.as_ref().unchecked_ref(),
229            millis,
230        )
231        .expect("failed to set interval");
232    closure.forget();
233    let handle: IntervalHandle = IntervalHandle::new(interval_id);
234    inner.get_mut_cleanups().push(Box::new(move || {
235        let Some(cleanup_window) = web_sys::window() else {
236            return;
237        };
238        cleanup_window.clear_interval_with_handle(interval_id);
239    }));
240    if index < inner.get_hooks().len() {
241        inner.get_mut_hooks()[index] = Box::new(handle);
242    } else {
243        inner.get_mut_hooks().push(Box::new(handle));
244    }
245    handle
246}