Skip to main content

euv_core/reactive/hook/
impl.rs

1use crate::*;
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
46/// Clones the hook context, sharing the same inner state.
47///
48/// All clones share the same underlying `Rc<RefCell<HookContextInner>>`,
49/// so modifications through one clone are visible through all others.
50///
51/// # Returns
52///
53/// - `Self` - A new `HookContext` sharing the same inner state.
54impl Clone for HookContext {
55    fn clone(&self) -> Self {
56        Self::new(self.get_inner().clone())
57    }
58}
59
60/// Provides a default empty hook context.
61///
62/// Creates a fresh `Rc<RefCell<HookContextInner>>` with default values
63/// (empty hook list, zero hook index, empty cleanup list).
64///
65/// # Returns
66///
67/// - `Self` - A new `HookContext` with default inner state.
68impl Default for HookContext {
69    fn default() -> Self {
70        Self::new(Rc::new(RefCell::new(HookContextInner::default())))
71    }
72}
73
74/// Implementation of interval handle lifecycle management.
75impl IntervalHandle {
76    /// Cancels the associated browser interval timer.
77    ///
78    /// Calls `window.clearInterval` with the stored interval ID.
79    /// After calling this method the interval callback will no longer fire.
80    ///
81    /// # Panics
82    ///
83    /// Panics if `window()` is unavailable on the current platform.
84    pub fn clear(&self) {
85        if let Some(cleanup_window) = web_sys::window() {
86            cleanup_window.clear_interval_with_handle(self.get_interval_id());
87        }
88    }
89}
90
91/// Associated functions for hook context management.
92///
93/// These are crate-internal static methods for managing the active hook
94/// context, creating signals, registering cleanups, and scheduling intervals.
95impl HookContext {
96    /// Returns a shared reference to the current hook context global state.
97    ///
98    /// SAFETY: Must only be called from the main thread (WASM single-threaded context).
99    #[allow(static_mut_refs)]
100    fn try_get_current() -> &'static Option<HookContextRc> {
101        unsafe { &*CURRENT_HOOK_CONTEXT.get_0().get() }
102    }
103
104    /// Returns a mutable reference to the current hook context global state.
105    ///
106    /// SAFETY: Must only be called from the main thread (WASM single-threaded context).
107    #[allow(static_mut_refs)]
108    fn try_get_current_mut() -> &'static mut Option<HookContextRc> {
109        unsafe { &mut *CURRENT_HOOK_CONTEXT.get_0().get() }
110    }
111
112    /// Returns the currently active `HookContext`.
113    ///
114    /// If no hook context has been set, creates and stores a default one
115    /// in the global `CURRENT_HOOK_CONTEXT` cell so subsequent calls
116    /// return the same instance.
117    ///
118    /// # Returns
119    ///
120    /// - `HookContext` - The currently active hook context.
121    pub(crate) fn current() -> HookContext {
122        match Self::try_get_current() {
123            Some(hook_context_rc) => HookContext::new(hook_context_rc.clone()),
124            None => {
125                let rc: HookContextRc = Rc::new(RefCell::new(HookContextInner::default()));
126                *Self::try_get_current_mut() = Some(rc.clone());
127                HookContext::new(rc)
128            }
129        }
130    }
131
132    /// Runs a closure with the given `HookContext` set as the active context.
133    ///
134    /// Saves the previous context, sets the new one, executes the closure,
135    /// and restores the previous context afterward.
136    ///
137    /// # Arguments
138    ///
139    /// - `HookContext` - The hook context to set as active during closure execution.
140    /// - `FnOnce() -> R` - The closure to execute with the given context.
141    ///
142    /// # Returns
143    ///
144    /// - `R` - The result of the closure execution.
145    pub(crate) fn with<F, R>(context: HookContext, callback: F) -> R
146    where
147        F: FnOnce() -> R,
148    {
149        let previous: Option<HookContextRc> = Self::try_get_current_mut().take();
150        *Self::try_get_current_mut() = Some(context.get_inner().clone());
151        let result: R = callback();
152        *Self::try_get_current_mut() = previous;
153        result
154    }
155
156    /// Creates a new reactive signal with the given initial value.
157    ///
158    /// Uses the current `HookContext` to maintain signal identity across
159    /// re-renders. On the first call at a given hook index, the signal
160    /// is created with `init()` and stored. On subsequent re-renders,
161    /// the existing signal at that index is returned unchanged.
162    ///
163    /// # Arguments
164    ///
165    /// - `FnOnce() -> T` - A closure that computes the initial value of the signal.
166    ///
167    /// # Returns
168    ///
169    /// - `Signal<T>` - A reactive signal containing the initialized or existing value.
170    pub(crate) fn signal<T, F>(init: F) -> Signal<T>
171    where
172        T: Clone + PartialEq + 'static,
173        F: FnOnce() -> T,
174    {
175        let hook_context: HookContext = Self::current();
176        let Ok(mut inner) = hook_context.get_inner().try_borrow_mut() else {
177            return Signal::create(init());
178        };
179        let index: usize = inner.get_hook_index();
180        inner.set_hook_index(index + 1);
181        if index < inner.get_hooks().len()
182            && let Some(existing) = inner.get_hooks()[index].downcast_ref::<Signal<T>>()
183        {
184            return *existing;
185        }
186        let signal: Signal<T> = Signal::create(init());
187        inner
188            .get_mut_cleanups()
189            .push(Box::new(move || signal.deactivate()));
190        if index < inner.get_hooks().len() {
191            inner.get_mut_hooks()[index] = Box::new(signal);
192        } else {
193            inner.get_mut_hooks().push(Box::new(signal));
194        }
195        signal
196    }
197
198    /// Registers a cleanup callback that will be executed when the current
199    /// hook context is cleared (e.g., when a `match` arm switches).
200    ///
201    /// This is useful for cleaning up side effects like intervals, timeouts,
202    /// or subscriptions that are not automatically managed by signals.
203    ///
204    /// The cleanup callback is only registered once on the first render.
205    /// On subsequent re-renders at the same hook index, this is a no-op.
206    ///
207    /// # Arguments
208    ///
209    /// - `FnOnce() + 'static` - The cleanup callback to execute on context teardown.
210    pub(crate) fn cleanup<F>(cleanup: F)
211    where
212        F: FnOnce() + 'static,
213    {
214        let hook_context: HookContext = Self::current();
215        let Ok(mut inner) = hook_context.get_inner().try_borrow_mut() else {
216            return;
217        };
218        let index: usize = inner.get_hook_index();
219        inner.set_hook_index(index + 1);
220        if index < inner.get_hooks().len() {
221            return;
222        }
223        inner.get_mut_cleanups().push(Box::new(cleanup));
224        inner.get_mut_hooks().push(Box::new(()));
225    }
226
227    /// Registers a `window.addEventListener` callback using event delegation,
228    /// automatically removed when the hook context is cleared.
229    ///
230    /// Uses the global window event proxy registry so that only one
231    /// `window.addEventListener` call is made per event name regardless of
232    /// how many components listen to the same event. On cleanup, only the
233    /// handler entry is removed from the proxy registry; the shared window
234    /// listener remains active for other consumers.
235    ///
236    /// The event listener is only registered once on the first render.
237    /// On subsequent re-renders at the same hook index, this is a no-op.
238    ///
239    /// # Arguments
240    ///
241    /// - `E: AsRef<str>` - The event name to listen for (e.g., "hashchange", "popstate", "resize").
242    /// - `FnMut() + 'static` - The callback to invoke when the event fires.
243    pub(crate) fn window_event<E, F>(event_name: E, callback: F)
244    where
245        E: AsRef<str>,
246        F: FnMut() + 'static,
247    {
248        let event_name: &str = event_name.as_ref();
249        let hook_context: HookContext = Self::current();
250        let Ok(mut inner) = hook_context.get_inner().try_borrow_mut() else {
251            return;
252        };
253        let index: usize = inner.get_hook_index();
254        inner.set_hook_index(index + 1);
255        if index < inner.get_hooks().len() {
256            return;
257        }
258        let event_name_owned: String = event_name.to_owned();
259        let handler_id: usize = Registry::register_window_event(event_name, callback);
260        inner.get_mut_cleanups().push(Box::new(move || {
261            Registry::unregister_window_event(&event_name_owned, handler_id);
262        }));
263        inner.get_mut_hooks().push(Box::new(()));
264    }
265
266    /// Creates a recurring interval that invokes the given closure at the
267    /// specified period, returning an `IntervalHandle` that is automatically
268    /// cleared when the hook context is cleared (i.e., when the component
269    /// unmounts or a `match` arm switches).
270    ///
271    /// Unlike calling `set_interval_with_callback_and_timeout_and_arguments_0`
272    /// + `Closure::forget()` manually, this hook ensures the interval is
273    ///   properly cleaned up, preventing memory leaks and stale callbacks.
274    ///
275    /// The interval is only created once on the first render.
276    /// On subsequent re-renders at the same hook index, the existing handle
277    /// is returned unchanged.
278    ///
279    /// # Arguments
280    ///
281    /// - `i32` - The interval period in milliseconds.
282    /// - `FnMut() + 'static` - The closure to invoke on each interval tick.
283    ///
284    /// # Returns
285    ///
286    /// - `IntervalHandle` - A handle that can be used to cancel the interval early.
287    ///
288    /// # Panics
289    ///
290    /// Panics if `window()` is unavailable on the current platform.
291    pub(crate) fn interval<F>(millis: i32, callback: F) -> IntervalHandle
292    where
293        F: FnMut() + 'static,
294    {
295        let hook_context: HookContext = Self::current();
296        let Ok(mut inner) = hook_context.get_inner().try_borrow_mut() else {
297            return IntervalHandle::new(0);
298        };
299        let index: usize = inner.get_hook_index();
300        inner.set_hook_index(index + 1);
301        if index < inner.get_hooks().len()
302            && let Some(existing) = inner.get_hooks()[index].downcast_ref::<IntervalHandle>()
303        {
304            return *existing;
305        }
306        let closure: Closure<dyn FnMut()> = Closure::wrap(Box::new(callback));
307        let window: Window = window().expect("no global window exists");
308        let interval_id: i32 = window
309            .set_interval_with_callback_and_timeout_and_arguments_0(
310                closure.as_ref().unchecked_ref(),
311                millis,
312            )
313            .expect("failed to set interval");
314        closure.forget();
315        let handle: IntervalHandle = IntervalHandle::new(interval_id);
316        inner.get_mut_cleanups().push(Box::new(move || {
317            let Some(cleanup_window) = web_sys::window() else {
318                return;
319            };
320            cleanup_window.clear_interval_with_handle(interval_id);
321        }));
322        if index < inner.get_hooks().len() {
323            inner.get_mut_hooks()[index] = Box::new(handle);
324        } else {
325            inner.get_mut_hooks().push(Box::new(handle));
326        }
327        handle
328    }
329}