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}