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