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}