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}