Skip to main content

euv_core/reactive/hook/
impl.rs

1use super::*;
2
3/// Implementation of hook context lifecycle and hook index management.
4impl HookContext {
5    /// Resets the hook index for a new render cycle.
6    ///
7    /// Sets the internal hook index back to zero so that subsequent
8    /// `use_signal` calls start indexing from the beginning of the hook list.
9    pub fn reset_index(&mut self) {
10        if let Ok(mut inner) = self.get_inner().try_borrow_mut() {
11            inner.set_hook_index(0);
12        }
13    }
14
15    /// Notifies the hook context that a match arm is being entered.
16    ///
17    /// If the arm index has changed, all existing hooks and cleanups
18    /// are cleared and re-initialized for the new arm. If the arm
19    /// is unchanged, only the hook index is reset.
20    ///
21    /// # Arguments
22    ///
23    /// - `usize` - The index of the new match arm.
24    pub fn switch_arm(&mut self, changed: usize) {
25        let cleanups: Vec<Box<dyn FnOnce()>>;
26        {
27            let Ok(mut inner) = self.get_inner().try_borrow_mut() else {
28                return;
29            };
30            if inner.get_arm_changed() == changed {
31                drop(inner);
32                self.reset_index();
33                return;
34            }
35            cleanups = take(inner.get_mut_cleanups());
36            inner.get_mut_hooks().clear();
37            inner.set_arm_changed(changed);
38        }
39        for cleanup in cleanups {
40            cleanup();
41        }
42        // SPA reclamation: every cleanup callback has just run its
43        // `Signal::deactivate` (for signals owned by the torn-down hook
44        // context), which removes the source from each bridge's
45        // dependency set in `BridgeRefsCell`. Any bridge whose DOM
46        // element was detached BEFORE the source deactivated is now
47        // an orphan (empty dep set, not in `SIGNAL_INNER_REGISTRY`).
48        // This is the natural collection moment for those orphan
49        // bridge allocations — drive a bounded sweep here so the
50        // free happens immediately rather than waiting for the next
51        // page transition or a manual sweep from user code.
52        //
53        // `try_reclaim_inactive` returns the number of allocations
54        // reclaimed; we discard the count because the call is
55        // opportunistic — failing to reclaim in this frame just
56        // defers the work to a later sweep, never blocking the UI.
57        let _freed: usize = Signal::<String>::try_reclaim_inactive(usize::MAX);
58        self.reset_index();
59    }
60
61    /// Creates or reuses a `NodeRef<T>` at the current hook index.
62    ///
63    /// On the first call at a given hook index, a fresh empty `NodeRef`
64    /// is stored. On subsequent re-renders the same instance is returned,
65    /// so a ref cloned into a closure stays attached to the live DOM
66    /// element across renders.
67    ///
68    /// The element type `T` is a phantom marker only — we downcast the
69    /// stored `Box<dyn Any>` back to `NodeRef<T>` using the same pattern
70    /// as `Signal::signal` above. Note that two calls at the same hook
71    /// index with different `T` would still match (both are `NodeRef<...>`)
72    /// because the `downcast_ref` ignores the phantom parameter.
73    ///
74    /// # Returns
75    ///
76    /// - `NodeRef<T>` - A `NodeRef<T>` value.
77    pub fn noderef<T>() -> NodeRef<T>
78    where
79        T: ?Sized + 'static,
80    {
81        let hook_context: HookContext = Self::current();
82        let Ok(mut inner) = hook_context.get_inner().try_borrow_mut() else {
83            // Borrow failed (renderer re-entered); fall back to a fresh
84            // empty ref so the caller still gets a usable handle.
85            return NodeRef::new();
86        };
87        let index: usize = inner.get_hook_index();
88        inner.set_hook_index(index + 1);
89        if index < inner.get_hooks().len() {
90            // Re-render path: try to reuse the existing NodeRef stored at
91            // this hook index. If a different hook type was at this slot
92            // (e.g. user swapped `use_signal` for `use_node_ref`), replace
93            // it with a fresh ref rather than panicking.
94            if let Some(existing) = inner.get_hooks()[index].downcast_ref::<NodeRef<T>>() {
95                return existing.clone();
96            }
97            let new_ref: NodeRef<T> = NodeRef::new();
98            inner.get_mut_hooks()[index] = Box::new(new_ref.clone());
99            return new_ref;
100        }
101        let new_ref: NodeRef<T> = NodeRef::new();
102        inner.get_mut_hooks().push(Box::new(new_ref.clone()));
103        new_ref
104    }
105}
106
107/// Clones the hook context, sharing the same inner state.
108///
109/// All clones share the same underlying `Rc<RefCell<HookContextInner>>`,
110/// so modifications through one clone are visible through all others.
111///
112/// # Returns
113///
114/// - `Self` - A new `HookContext` sharing the same inner state.
115impl Clone for HookContext {
116    /// Clones the [`HookContext`] by reusing shared, cheap-to-clone state where possible.
117    fn clone(&self) -> Self {
118        Self::new(self.get_inner().clone())
119    }
120}
121
122/// Provides a default empty hook context.
123///
124/// Creates a fresh `Rc<RefCell<HookContextInner>>` with default values
125/// (empty hook list, zero hook index, empty cleanup list).
126///
127/// # Returns
128///
129/// - `Self` - A new `HookContext` with default inner state.
130impl Default for HookContext {
131    /// Constructs a default [`HookContext`] value.
132    fn default() -> Self {
133        Self::new(Rc::new(RefCell::new(HookContextInner::default())))
134    }
135}
136
137/// Implementation of interval handle lifecycle management.
138impl IntervalHandle {
139    /// Cancels the associated browser interval timer.
140    ///
141    /// Calls `window.clearInterval` with the stored interval ID.
142    /// After calling this method the interval callback will no longer fire.
143    ///
144    /// # Panics
145    ///
146    /// Panics if `window()` is unavailable on the current platform.
147    pub fn clear(&self) {
148        if let Some(cleanup_window) = web_sys::window() {
149            cleanup_window.clear_interval_with_handle(self.get_interval_id());
150        }
151    }
152}
153
154/// Associated functions for hook context management.
155///
156/// These are crate-internal static methods for managing the active hook
157/// context, creating signals, registering cleanups, and scheduling intervals.
158impl HookContext {
159    /// Returns a shared reference to the current hook context global state.
160    ///
161    /// SAFETY: Must only be called from the main thread (WASM single-threaded context).
162    #[allow(static_mut_refs)]
163    fn try_get_current() -> &'static Option<HookContextRc> {
164        unsafe { &*CURRENT_HOOK_CONTEXT.get_0().get() }
165    }
166
167    /// Returns a mutable reference to the current hook context global state.
168    ///
169    /// SAFETY: Must only be called from the main thread (WASM single-threaded context).
170    #[allow(static_mut_refs)]
171    fn try_get_mut_current() -> &'static mut Option<HookContextRc> {
172        unsafe { &mut *CURRENT_HOOK_CONTEXT.get_0().get() }
173    }
174
175    /// Returns the currently active `HookContext`.
176    ///
177    /// If no hook context has been set, creates and stores a default one
178    /// in the global `CURRENT_HOOK_CONTEXT` cell so subsequent calls
179    /// return the same instance.
180    ///
181    /// # Returns
182    ///
183    /// - `HookContext` - The currently active hook context.
184    pub fn current() -> HookContext {
185        match Self::try_get_current() {
186            Some(hook_context_rc) => HookContext::new(hook_context_rc.clone()),
187            None => {
188                let rc: HookContextRc = Rc::new(RefCell::new(HookContextInner::default()));
189                *Self::try_get_mut_current() = Some(rc.clone());
190                HookContext::new(rc)
191            }
192        }
193    }
194
195    /// Runs a closure with the given `HookContext` set as the active context.
196    ///
197    /// Saves the previous context, sets the new one, executes the closure,
198    /// and restores the previous context afterward.
199    ///
200    /// # Arguments
201    ///
202    /// - `HookContext` - The hook context to set as active during closure execution.
203    /// - `F: FnOnce() -> R` - The closure to execute with the given context.
204    ///
205    /// # Returns
206    ///
207    /// - `R` - The result of the closure execution.
208    pub fn with<F, R>(context: HookContext, callback: F) -> R
209    where
210        F: FnOnce() -> R,
211    {
212        let previous: Option<HookContextRc> = Self::try_get_mut_current().take();
213        *Self::try_get_mut_current() = Some(context.get_inner().clone());
214        let result: R = callback();
215        *Self::try_get_mut_current() = previous;
216        result
217    }
218
219    /// Creates a new reactive signal with the given initial value.
220    ///
221    /// Uses the current `HookContext` to maintain signal identity across
222    /// re-renders. On the first call at a given hook index, the signal
223    /// is created with `init()` and stored. On subsequent re-renders,
224    /// the existing signal at that index is returned unchanged.
225    ///
226    /// # Arguments
227    ///
228    /// - `FnOnce() -> T` - A closure that computes the initial value of the signal.
229    ///
230    /// # Returns
231    ///
232    /// - `Signal<T>` - A reactive signal containing the initialized or existing value.
233    pub fn signal<T, F>(init: F) -> Signal<T>
234    where
235        T: Clone + PartialEq + 'static,
236        F: FnOnce() -> T,
237    {
238        let hook_context: HookContext = Self::current();
239        let Ok(mut inner) = hook_context.get_inner().try_borrow_mut() else {
240            return Signal::create(init());
241        };
242        let index: usize = inner.get_hook_index();
243        inner.set_hook_index(index + 1);
244        if index < inner.get_hooks().len()
245            && let Some(existing) = inner.get_hooks()[index].downcast_ref::<Signal<T>>()
246        {
247            return *existing;
248        }
249        let signal: Signal<T> = Signal::create(init());
250        inner
251            .get_mut_cleanups()
252            .push(Box::new(move || signal.deactivate()));
253        if index < inner.get_hooks().len() {
254            inner.get_mut_hooks()[index] = Box::new(signal);
255        } else {
256            inner.get_mut_hooks().push(Box::new(signal));
257        }
258        signal
259    }
260
261    /// Registers a cleanup callback that will be executed when the current
262    /// hook context is cleared (e.g., when a `match` arm switches).
263    ///
264    /// This is useful for cleaning up side effects like intervals, timeouts,
265    /// or subscriptions that are not automatically managed by signals.
266    ///
267    /// The cleanup callback is only registered once on the first render.
268    /// On subsequent re-renders at the same hook index, this is a no-op.
269    ///
270    /// # Arguments
271    ///
272    /// - `FnOnce() + 'static` - The cleanup callback to execute on context teardown.
273    pub fn cleanup<F>(cleanup: F)
274    where
275        F: FnOnce() + 'static,
276    {
277        let hook_context: HookContext = Self::current();
278        let Ok(mut inner) = hook_context.get_inner().try_borrow_mut() else {
279            return;
280        };
281        let index: usize = inner.get_hook_index();
282        inner.set_hook_index(index + 1);
283        if index < inner.get_hooks().len() {
284            return;
285        }
286        inner.get_mut_cleanups().push(Box::new(cleanup));
287        inner.get_mut_hooks().push(Box::new(()));
288    }
289
290    /// Registers a `window.addEventListener` callback using event delegation,
291    /// automatically removed when the hook context is cleared.
292    ///
293    /// Uses the global window event proxy registry so that only one
294    /// `window.addEventListener` call is made per event name regardless of
295    /// how many components listen to the same event. On cleanup, only the
296    /// handler entry is removed from the proxy registry; the shared window
297    /// listener remains active for other consumers.
298    ///
299    /// The event listener is only registered once on the first render.
300    /// On subsequent re-renders at the same hook index, this is a no-op.
301    ///
302    /// # Arguments
303    ///
304    /// - `E: AsRef<str>` - The event name to listen for (e.g., "hashchange", "popstate", "resize").
305    /// - `FnMut() + 'static` - The callback to invoke when the event fires.
306    pub fn window_event<E, F>(event_name: E, callback: F)
307    where
308        E: AsRef<str>,
309        F: FnMut() + 'static,
310    {
311        let event_name: &str = event_name.as_ref();
312        let hook_context: HookContext = Self::current();
313        let Ok(mut inner) = hook_context.get_inner().try_borrow_mut() else {
314            return;
315        };
316        let index: usize = inner.get_hook_index();
317        inner.set_hook_index(index + 1);
318        if index < inner.get_hooks().len() {
319            return;
320        }
321        let event_name_owned: String = event_name.to_owned();
322        let handler_id: usize = Registry::register_window_event(event_name, callback);
323        inner.get_mut_cleanups().push(Box::new(move || {
324            Registry::unregister_window_event(&event_name_owned, handler_id);
325        }));
326        inner.get_mut_hooks().push(Box::new(()));
327    }
328
329    /// Creates a recurring interval that invokes the given closure at the
330    /// specified period, returning an `IntervalHandle` that is automatically
331    /// cleared when the hook context is cleared (i.e., when the component
332    /// unmounts or a `match` arm switches).
333    ///
334    /// Unlike calling `set_interval_with_callback_and_timeout_and_arguments_0`
335    /// + `Closure::forget()` manually, this hook ensures the interval is
336    ///   properly cleaned up, preventing memory leaks and stale callbacks.
337    ///
338    /// The interval is only created once on the first render.
339    /// On subsequent re-renders at the same hook index, the existing handle
340    /// is returned unchanged.
341    ///
342    /// # Arguments
343    ///
344    /// - `i32` - The interval period in milliseconds.
345    /// - `FnMut() + 'static` - The closure to invoke on each interval tick.
346    ///
347    /// # Returns
348    ///
349    /// - `IntervalHandle` - A handle that can be used to cancel the interval early.
350    ///
351    /// # Panics
352    ///
353    /// Panics if `window()` is unavailable on the current platform.
354    pub fn interval<F>(millis: i32, callback: F) -> IntervalHandle
355    where
356        F: FnMut() + 'static,
357    {
358        let hook_context: HookContext = Self::current();
359        let Ok(mut inner) = hook_context.get_inner().try_borrow_mut() else {
360            return IntervalHandle::new(0);
361        };
362        let index: usize = inner.get_hook_index();
363        inner.set_hook_index(index + 1);
364        if index < inner.get_hooks().len()
365            && let Some(existing) = inner.get_hooks()[index].downcast_ref::<IntervalHandle>()
366        {
367            return *existing;
368        }
369        let closure: Closure<dyn FnMut()> = Closure::wrap(Box::new(callback));
370        let Some(window) = window() else {
371            closure.forget();
372            return IntervalHandle::new(0);
373        };
374        let Ok(interval_id) = window.set_interval_with_callback_and_timeout_and_arguments_0(
375            closure.as_ref().unchecked_ref(),
376            millis,
377        ) else {
378            closure.forget();
379            return IntervalHandle::new(0);
380        };
381        closure.forget();
382        let handle: IntervalHandle = IntervalHandle::new(interval_id);
383        inner.get_mut_cleanups().push(Box::new(move || {
384            let Some(cleanup_window) = web_sys::window() else {
385                return;
386            };
387            cleanup_window.clear_interval_with_handle(interval_id);
388        }));
389        if index < inner.get_hooks().len() {
390            inner.get_mut_hooks()[index] = Box::new(handle);
391        } else {
392            inner.get_mut_hooks().push(Box::new(handle));
393        }
394        handle
395    }
396}
397
398/// Inherent implementation of [`HookContext`].
399impl HookContext {
400    /// Registers a hook value with the current hook context and returns
401    /// the existing instance if one was stored at this index from a
402    /// previous render cycle.
403    ///
404    /// This is the public extension point for custom hook types.
405    /// `ui` and downstream crates implement `use_form`, `use_i18n`, etc.
406    /// on top of this primitive instead of poking at the hook array
407    /// directly. `factory` runs once per hook slot on the first render;
408    /// subsequent renders in the same arm return the previously stored
409    /// instance.
410    ///
411    /// # Arguments
412    ///
413    /// - `F: FnOnce() -> T` - Constructor that produces a fresh value
414    ///   of type `T` when the slot has never been written.
415    ///
416    /// # Returns
417    ///
418    /// - `T: Clone + 'static` - Either the previously-stored
419    ///   value (cheap clone / copy) or a fresh one from
420    ///   `factory`.
421    pub fn use_hook<T, F>(factory: F) -> T
422    where
423        F: FnOnce() -> T,
424        T: Clone + 'static,
425    {
426        let hook_context: HookContext = Self::current();
427        let Ok(mut inner) = hook_context.get_inner().try_borrow_mut() else {
428            return factory();
429        };
430        let index: usize = inner.get_hook_index();
431        inner.set_hook_index(index + 1);
432        if index < inner.get_hooks().len()
433            && let Some(existing) = inner.get_hooks()[index].downcast_ref::<T>()
434        {
435            return existing.clone();
436        }
437        let state: T = factory();
438        if index < inner.get_hooks().len() {
439            inner.get_mut_hooks()[index] = Box::new(state.clone());
440        } else {
441            inner.get_mut_hooks().push(Box::new(state.clone()));
442        }
443        state
444    }
445}