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 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    // 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/// - `&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<F>(event_name: &str, callback: F)
162where
163    F: FnMut() + 'static,
164{
165    let hook_context: HookContext = get_current_hook_context();
166    let Ok(mut inner) = hook_context.get_inner().try_borrow_mut() else {
167        return;
168    };
169    let index: usize = inner.get_hook_index();
170    inner.set_hook_index(index + 1);
171    if index < inner.get_hooks().len() {
172        return;
173    }
174    let event_name_owned: String = event_name.to_string();
175    let handler_id: usize = register_window_event_handler(event_name, callback);
176    inner.get_mut_cleanups().push(Box::new(move || {
177        unregister_window_event_handler(&event_name_owned, handler_id);
178    }));
179    inner.get_mut_hooks().push(Box::new(()));
180}
181
182/// Creates a recurring interval that invokes the given closure at the
183/// specified period, returning an `IntervalHandle` that is automatically
184/// cleared when the hook context is cleared (i.e., when the component
185/// unmounts or a `match` arm switches).
186///
187/// Unlike calling `set_interval_with_callback_and_timeout_and_arguments_0`
188/// + `Closure::forget()` manually, this hook ensures the interval is
189///   properly cleaned up, preventing memory leaks and stale callbacks.
190///
191/// The interval is only created once on the first render.
192/// On subsequent re-renders at the same hook index, the existing handle
193/// is returned unchanged.
194///
195/// # Arguments
196///
197/// - `i32` - The interval period in milliseconds.
198/// - `FnMut() + 'static` - The closure to invoke on each interval tick.
199///
200/// # Returns
201///
202/// - `IntervalHandle` - A handle that can be used to cancel the interval early.
203///
204/// # Panics
205///
206/// Panics if `window()` is unavailable on the current platform.
207pub fn use_interval<F>(millis: i32, callback: F) -> IntervalHandle
208where
209    F: FnMut() + 'static,
210{
211    let hook_context: HookContext = get_current_hook_context();
212    let Ok(mut inner) = hook_context.get_inner().try_borrow_mut() else {
213        return IntervalHandle::new(0);
214    };
215    let index: usize = inner.get_hook_index();
216    inner.set_hook_index(index + 1);
217    if index < inner.get_hooks().len()
218        && let Some(existing) = inner.get_hooks()[index].downcast_ref::<IntervalHandle>()
219    {
220        return *existing;
221    }
222    let closure: Closure<dyn FnMut()> = Closure::wrap(Box::new(callback));
223    let window: Window = window().expect("no global window exists");
224    let interval_id: i32 = window
225        .set_interval_with_callback_and_timeout_and_arguments_0(
226            closure.as_ref().unchecked_ref(),
227            millis,
228        )
229        .expect("failed to set interval");
230    closure.forget();
231    let handle: IntervalHandle = IntervalHandle::new(interval_id);
232    inner.get_mut_cleanups().push(Box::new(move || {
233        let Some(cleanup_window) = web_sys::window() else {
234            return;
235        };
236        cleanup_window.clear_interval_with_handle(interval_id);
237    }));
238    if index < inner.get_hooks().len() {
239        inner.get_mut_hooks()[index] = Box::new(handle);
240    } else {
241        inner.get_mut_hooks().push(Box::new(handle));
242    }
243    handle
244}