reratui_hooks/effect/mod.rs
1use parking_lot::Mutex;
2use std::any::Any;
3use std::sync::Arc;
4
5#[cfg(test)]
6mod tests;
7
8use crate::hook_context::with_hook_context;
9use crate::panic_handler::spawn_catch_panic;
10
11/// Trait for types that can be used as effect dependencies
12/// This enables dependency comparison for conditional effect re-execution
13pub trait EffectDependencies: Any + Send + Sync {
14 /// Compare this dependency set with another for equality
15 fn deps_eq(&self, other: &dyn EffectDependencies) -> bool;
16
17 /// Clone this dependency set as a boxed trait object
18 fn clone_deps(&self) -> Box<dyn EffectDependencies>;
19
20 /// Get a debug representation of the dependencies
21 fn debug_deps(&self) -> String;
22
23 /// Get a hash of the dependencies for fast comparison
24 /// This enables optimization where we can quickly check if dependencies
25 /// might have changed before doing expensive equality comparisons
26 fn deps_hash(&self) -> u64;
27}
28
29// Add as_any method to EffectDependencies trait
30impl dyn EffectDependencies {
31 pub fn as_any(&self) -> &dyn Any {
32 self
33 }
34}
35
36// Implement EffectDependencies for common types:
37
38// Implement EffectDependencies for unit type () - represents empty dependencies
39impl EffectDependencies for () {
40 fn deps_eq(&self, other: &dyn EffectDependencies) -> bool {
41 other.as_any().downcast_ref::<()>().is_some()
42 }
43
44 fn clone_deps(&self) -> Box<dyn EffectDependencies> {
45 Box::new(())
46 }
47
48 fn debug_deps(&self) -> String {
49 "()".to_string()
50 }
51
52 fn deps_hash(&self) -> u64 {
53 // Unit type always has the same hash
54 0
55 }
56}
57
58// Implement for primitive types
59macro_rules! impl_effect_deps_for_primitive {
60 ($($t:ty),*) => {
61 $(
62 impl EffectDependencies for $t {
63 fn deps_eq(&self, other: &dyn EffectDependencies) -> bool {
64 if let Some(other_val) = other.as_any().downcast_ref::<$t>() {
65 self == other_val
66 } else {
67 false
68 }
69 }
70
71 fn clone_deps(&self) -> Box<dyn EffectDependencies> {
72 Box::new(*self)
73 }
74
75 fn debug_deps(&self) -> String {
76 format!("{:?}", self)
77 }
78
79 fn deps_hash(&self) -> u64 {
80 use std::collections::hash_map::DefaultHasher;
81 use std::hash::{Hash, Hasher};
82 let mut hasher = DefaultHasher::new();
83 self.hash(&mut hasher);
84 hasher.finish()
85 }
86 }
87 )*
88 };
89}
90
91impl_effect_deps_for_primitive!(
92 i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize, bool, char
93);
94
95// Special implementations for floating point types (which don't implement Hash)
96impl EffectDependencies for f32 {
97 fn deps_eq(&self, other: &dyn EffectDependencies) -> bool {
98 if let Some(other_val) = other.as_any().downcast_ref::<f32>() {
99 self == other_val
100 } else {
101 false
102 }
103 }
104
105 fn clone_deps(&self) -> Box<dyn EffectDependencies> {
106 Box::new(*self)
107 }
108
109 fn debug_deps(&self) -> String {
110 format!("{:?}", self)
111 }
112
113 fn deps_hash(&self) -> u64 {
114 // For f32, we use the bit representation for hashing
115 self.to_bits() as u64
116 }
117}
118
119impl EffectDependencies for f64 {
120 fn deps_eq(&self, other: &dyn EffectDependencies) -> bool {
121 if let Some(other_val) = other.as_any().downcast_ref::<f64>() {
122 self == other_val
123 } else {
124 false
125 }
126 }
127
128 fn clone_deps(&self) -> Box<dyn EffectDependencies> {
129 Box::new(*self)
130 }
131
132 fn debug_deps(&self) -> String {
133 format!("{:?}", self)
134 }
135
136 fn deps_hash(&self) -> u64 {
137 // For f64, we use the bit representation for hashing
138 self.to_bits()
139 }
140}
141
142impl EffectDependencies for String {
143 fn deps_eq(&self, other: &dyn EffectDependencies) -> bool {
144 if let Some(other_val) = other.as_any().downcast_ref::<String>() {
145 self == other_val
146 } else {
147 false
148 }
149 }
150
151 fn clone_deps(&self) -> Box<dyn EffectDependencies> {
152 Box::new(self.clone())
153 }
154
155 fn debug_deps(&self) -> String {
156 format!("{:?}", self)
157 }
158
159 fn deps_hash(&self) -> u64 {
160 use std::collections::hash_map::DefaultHasher;
161 use std::hash::{Hash, Hasher};
162 let mut hasher = DefaultHasher::new();
163 self.hash(&mut hasher);
164 hasher.finish()
165 }
166}
167
168impl EffectDependencies for &'static str {
169 fn deps_eq(&self, other: &dyn EffectDependencies) -> bool {
170 if let Some(other_val) = other.as_any().downcast_ref::<&'static str>() {
171 self == other_val
172 } else {
173 false
174 }
175 }
176
177 fn clone_deps(&self) -> Box<dyn EffectDependencies> {
178 Box::new(*self)
179 }
180
181 fn debug_deps(&self) -> String {
182 format!("{:?}", self)
183 }
184
185 fn deps_hash(&self) -> u64 {
186 use std::collections::hash_map::DefaultHasher;
187 use std::hash::{Hash, Hasher};
188 let mut hasher = DefaultHasher::new();
189 self.hash(&mut hasher);
190 hasher.finish()
191 }
192}
193
194/// Thread-safe wrapper for synchronous cleanup functions
195/// Ensures cleanup functions can be safely called from any thread
196pub struct CleanupFn {
197 /// The actual cleanup function, wrapped in Arc<Mutex<>> for thread safety
198 #[allow(clippy::type_complexity)]
199 cleanup: Arc<Mutex<Option<Box<dyn FnOnce() + Send + 'static>>>>,
200}
201
202/// Thread-safe wrapper for asynchronous cleanup functions
203/// Ensures async cleanup functions can be safely called from any thread
204pub struct AsyncCleanupFn {
205 /// The actual async cleanup function, wrapped in Arc<Mutex<>> for thread safety
206 #[allow(clippy::type_complexity)]
207 cleanup: Arc<
208 Mutex<
209 Option<
210 Box<
211 dyn FnOnce() -> std::pin::Pin<
212 Box<dyn std::future::Future<Output = ()> + Send + 'static>,
213 > + Send
214 + 'static,
215 >,
216 >,
217 >,
218 >,
219}
220
221impl CleanupFn {
222 /// Create a new cleanup function wrapper
223 pub fn new<F>(cleanup: F) -> Self
224 where
225 F: FnOnce() + Send + 'static,
226 {
227 Self {
228 cleanup: Arc::new(Mutex::new(Some(Box::new(cleanup)))),
229 }
230 }
231
232 /// Execute the cleanup function if it hasn't been called yet
233 /// This is idempotent - calling it multiple times is safe
234 pub fn cleanup(&self) {
235 if let Some(cleanup_fn) = self.cleanup.lock().take() {
236 cleanup_fn();
237 }
238 }
239}
240
241impl Clone for CleanupFn {
242 fn clone(&self) -> Self {
243 Self {
244 cleanup: self.cleanup.clone(),
245 }
246 }
247}
248
249impl AsyncCleanupFn {
250 /// Create a new async cleanup function wrapper
251 pub fn new<F, Fut>(cleanup: F) -> Self
252 where
253 F: FnOnce() -> Fut + Send + 'static,
254 Fut: std::future::Future<Output = ()> + Send + 'static,
255 {
256 Self {
257 cleanup: Arc::new(Mutex::new(Some(Box::new(move || {
258 Box::pin(cleanup())
259 as std::pin::Pin<Box<dyn std::future::Future<Output = ()> + Send + 'static>>
260 })))),
261 }
262 }
263
264 /// Execute the async cleanup function if it hasn't been called yet
265 /// This is idempotent - calling it multiple times is safe
266 pub async fn cleanup(&self) {
267 let cleanup_fn = self.cleanup.lock().take();
268 if let Some(cleanup_fn) = cleanup_fn {
269 cleanup_fn().await;
270 }
271 }
272}
273
274impl Clone for AsyncCleanupFn {
275 fn clone(&self) -> Self {
276 Self {
277 cleanup: self.cleanup.clone(),
278 }
279 }
280}
281
282/// Internal state for tracking synchronous effects
283struct EffectState {
284 /// Previous dependencies for comparison
285 prev_deps: Option<Box<dyn EffectDependencies>>,
286 /// Cleanup function from the previous effect run
287 cleanup: Option<CleanupFn>,
288 /// Whether this effect has been initialized
289 initialized: bool,
290}
291
292impl EffectState {
293 fn new() -> Self {
294 Self {
295 prev_deps: None,
296 cleanup: None,
297 initialized: false,
298 }
299 }
300}
301
302/// Internal state for tracking asynchronous effects
303struct AsyncEffectState {
304 /// Previous dependencies for comparison
305 prev_deps: Option<Box<dyn EffectDependencies>>,
306 /// Async cleanup function from the previous effect run
307 cleanup: Option<AsyncCleanupFn>,
308 /// Whether this effect has been initialized
309 initialized: bool,
310}
311
312impl AsyncEffectState {
313 fn new() -> Self {
314 Self {
315 prev_deps: None,
316 cleanup: None,
317 initialized: false,
318 }
319 }
320}
321
322// Implement EffectDependencies for tuples (up to 8 elements for practical use)
323macro_rules! impl_effect_deps_for_tuple {
324 ($($t:ident),*) => {
325 impl<$($t: EffectDependencies + Clone + PartialEq + std::fmt::Debug + 'static),*> EffectDependencies for ($($t,)*) {
326 fn deps_eq(&self, other: &dyn EffectDependencies) -> bool {
327 if let Some(other_tuple) = other.as_any().downcast_ref::<($($t,)*)>() {
328 self == other_tuple
329 } else {
330 false
331 }
332 }
333
334 fn clone_deps(&self) -> Box<dyn EffectDependencies> {
335 Box::new(self.clone())
336 }
337
338 fn debug_deps(&self) -> String {
339 format!("{:?}", self)
340 }
341
342 fn deps_hash(&self) -> u64 {
343 use std::collections::hash_map::DefaultHasher;
344 use std::hash::{Hash, Hasher};
345 let mut hasher = DefaultHasher::new();
346 // For tuples, we'll use the debug representation as a simple hash
347 // This is not the most efficient but works for all tuple types
348 self.debug_deps().hash(&mut hasher);
349 hasher.finish()
350 }
351 }
352 };
353}
354
355// Implement for tuples of various sizes
356impl_effect_deps_for_tuple!(T1);
357impl_effect_deps_for_tuple!(T1, T2);
358impl_effect_deps_for_tuple!(T1, T2, T3);
359impl_effect_deps_for_tuple!(T1, T2, T3, T4);
360impl_effect_deps_for_tuple!(T1, T2, T3, T4, T5);
361impl_effect_deps_for_tuple!(T1, T2, T3, T4, T5, T6);
362impl_effect_deps_for_tuple!(T1, T2, T3, T4, T5, T6, T7);
363impl_effect_deps_for_tuple!(T1, T2, T3, T4, T5, T6, T7, T8);
364
365// Implement EffectDependencies for Option<T> where T: EffectDependencies
366impl<T> EffectDependencies for Option<T>
367where
368 T: EffectDependencies + Clone + PartialEq + std::fmt::Debug + 'static,
369{
370 fn deps_eq(&self, other: &dyn EffectDependencies) -> bool {
371 if let Some(other_option) = other.as_any().downcast_ref::<Option<T>>() {
372 match (self, other_option) {
373 (None, None) => true,
374 (Some(a), Some(b)) => a.deps_eq(b),
375 _ => false,
376 }
377 } else {
378 false
379 }
380 }
381
382 fn clone_deps(&self) -> Box<dyn EffectDependencies> {
383 Box::new(self.clone())
384 }
385
386 fn debug_deps(&self) -> String {
387 match self {
388 None => "None".to_string(),
389 Some(value) => format!("Some({})", value.debug_deps()),
390 }
391 }
392
393 fn deps_hash(&self) -> u64 {
394 use std::collections::hash_map::DefaultHasher;
395 use std::hash::{Hash, Hasher};
396 let mut hasher = DefaultHasher::new();
397 match self {
398 None => 0u8.hash(&mut hasher),
399 Some(value) => {
400 1u8.hash(&mut hasher);
401 value.deps_hash().hash(&mut hasher);
402 }
403 }
404 hasher.finish()
405 }
406}
407
408/// React-style useAsyncEffect hook that provides async side effect management for components
409///
410/// This function provides async effects with React's useEffect behavior:
411/// - Async effects run after component render (not during)
412/// - Supports dependency arrays for conditional re-execution
413/// - Supports async cleanup functions returned from effects
414/// - Handles effect cleanup on component unmount
415/// - Supports empty dependency array for run-once effects
416/// - Supports None dependencies for effects that run on every render
417///
418/// # Error Handling
419///
420/// This function will panic if called outside of a component render context.
421/// Always ensure useAsyncEffect is called within a component function.
422///
423/// # Performance Notes
424///
425/// - Async effects are scheduled to run after render, not during
426/// - Dependency comparison uses PartialEq for efficient change detection
427/// - Async cleanup functions are automatically managed and called when needed
428/// - Multiple effects in the same component are executed in declaration order
429pub fn use_async_effect<Deps, F, Fut, C, CFut>(effect: F, deps: impl Into<Option<Deps>>)
430where
431 Deps: EffectDependencies + Clone + PartialEq + 'static,
432 F: FnOnce() -> Fut + Send + 'static,
433 Fut: std::future::Future<Output = Option<C>> + Send + 'static,
434 C: FnOnce() -> CFut + Send + 'static,
435 CFut: std::future::Future<Output = ()> + Send + 'static,
436{
437 let deps = deps.into();
438 with_hook_context(|ctx| {
439 let hook_index = ctx.next_hook_index();
440 let mut states = ctx.states.borrow_mut();
441
442 // Get or create async effect state for this hook
443 let effect_state = states
444 .entry(hook_index)
445 .or_insert_with(|| Box::new(AsyncEffectState::new()))
446 .downcast_mut::<AsyncEffectState>()
447 .expect("Async effect state type mismatch");
448
449 // Determine if effect should run
450 let should_run = match &deps {
451 None => {
452 // No dependencies - run on every render
453 true
454 }
455 Some(current_deps) => {
456 // Check if dependencies have changed
457 match &effect_state.prev_deps {
458 None => {
459 // First run - always execute
460 true
461 }
462 Some(prev_deps) => {
463 // Compare dependencies
464 !current_deps.deps_eq(prev_deps.as_ref())
465 }
466 }
467 }
468 };
469
470 if should_run {
471 // Run cleanup from previous effect if it exists
472 if let Some(cleanup) = effect_state.cleanup.take() {
473 // For now, we'll spawn the cleanup to run asynchronously
474 // In a real implementation, this would be properly scheduled
475 tokio::spawn(spawn_catch_panic(async move {
476 cleanup.cleanup().await;
477 }));
478 }
479
480 // Store new dependencies
481 if let Some(current_deps) = &deps {
482 effect_state.prev_deps = Some(current_deps.clone_deps());
483 } else {
484 effect_state.prev_deps = None;
485 }
486
487 // Schedule async effect to run after render
488 // For now, we'll spawn it immediately, but in a real implementation
489 // this would be scheduled to run after the render phase
490 tokio::spawn(spawn_catch_panic(async move {
491 if let Some(cleanup_fn) = effect().await {
492 // Store the cleanup function for later use
493 // In a real implementation, we'd need to store this back in the state
494 let _async_cleanup = AsyncCleanupFn::new(cleanup_fn);
495 // TODO: Store this cleanup function in the effect state
496 }
497 }));
498
499 effect_state.initialized = true;
500 }
501 });
502}
503
504/// React-style useEffect hook that provides synchronous side effect management for components
505///
506/// This function exactly mirrors React's useEffect behavior for synchronous effects:
507/// - Effects run after component render (not during)
508/// - Supports dependency arrays for conditional re-execution
509/// - Supports cleanup functions returned from effects
510/// - Handles effect cleanup on component unmount
511/// - Supports empty dependency array for run-once effects
512/// - Supports None dependencies for effects that run on every render
513///
514/// # Error Handling
515///
516/// This function will panic if called outside of a component render context.
517/// Always ensure useEffect is called within a component function.
518///
519/// # Performance Notes
520///
521/// - Effects are scheduled to run after render, not during
522/// - Dependency comparison uses PartialEq for efficient change detection
523/// - Cleanup functions are automatically managed and called when needed
524/// - Multiple effects in the same component are executed in declaration order
525pub fn use_effect<Deps, F, C>(effect: F, deps: impl Into<Option<Deps>>)
526where
527 Deps: EffectDependencies + Clone + PartialEq + 'static,
528 F: FnOnce() -> Option<C> + 'static,
529 C: FnOnce() + Send + 'static,
530{
531 let deps = deps.into();
532 with_hook_context(|ctx| {
533 let hook_index = ctx.next_hook_index();
534 let mut states = ctx.states.borrow_mut();
535
536 // Get or create effect state for this hook
537 let effect_state = states
538 .entry(hook_index)
539 .or_insert_with(|| Box::new(EffectState::new()))
540 .downcast_mut::<EffectState>()
541 .expect("Effect state type mismatch");
542
543 // Determine if effect should run
544 let should_run = match &deps {
545 None => {
546 // No dependencies - run on every render
547 true
548 }
549 Some(current_deps) => {
550 // Check if dependencies have changed
551 match &effect_state.prev_deps {
552 None => {
553 // First run - always execute
554 true
555 }
556 Some(prev_deps) => {
557 // Compare dependencies
558 !current_deps.deps_eq(prev_deps.as_ref())
559 }
560 }
561 }
562 };
563
564 if should_run {
565 // Run cleanup from previous effect if it exists
566 if let Some(cleanup) = effect_state.cleanup.take() {
567 cleanup.cleanup();
568 }
569
570 // Store new dependencies
571 if let Some(current_deps) = &deps {
572 effect_state.prev_deps = Some(current_deps.clone_deps());
573 } else {
574 effect_state.prev_deps = None;
575 }
576
577 // Schedule effect to run after render
578 // For now, we'll run it immediately, but in a real implementation
579 // this would be scheduled to run after the render phase
580 if let Some(cleanup_fn) = effect() {
581 effect_state.cleanup = Some(CleanupFn::new(cleanup_fn));
582 }
583
584 effect_state.initialized = true;
585 }
586 });
587}
588
589/// useEffect hook that runs on every render (no dependency tracking)
590///
591/// This is a convenience function for effects that should run on every render.
592/// It's equivalent to calling `use_effect(effect, None)`.
593///
594/// # Examples
595///
596/// ```rust,no_run
597/// # use reratui_hooks::effect::use_effect_always;
598/// use_effect_always(|| {
599/// println!("This effect runs on every render");
600/// || {} // Empty cleanup
601/// });
602///
603/// // With cleanup
604/// use_effect_always(|| {
605/// println!("Setting up resource");
606/// move || {
607/// println!("Cleaning up resource");
608/// }
609/// });
610/// ```
611pub fn use_effect_always<F, C>(effect: F)
612where
613 F: FnOnce() -> C + 'static,
614 C: FnOnce() + Send + 'static,
615{
616 use_effect::<(), _, C>(|| Some(effect()), None)
617}
618
619/// useEffect hook that runs only once (on mount)
620///
621/// This is a convenience function for effects that should run only once when
622/// the component mounts. It's equivalent to calling `use_effect(effect, ())`.
623///
624/// # Examples
625///
626/// ```rust,no_run
627/// # use reratui_hooks::effect::use_effect_once;
628/// use_effect_once(|| {
629/// println!("This effect runs only once");
630/// || {} // Empty cleanup
631/// });
632///
633/// // With cleanup
634/// use_effect_once(|| {
635/// println!("Setting up subscription");
636/// move || {
637/// println!("Cleaning up subscription");
638/// }
639/// });
640/// ```
641pub fn use_effect_once<F, C>(effect: F)
642where
643 F: FnOnce() -> C + 'static,
644 C: FnOnce() + Send + 'static,
645{
646 use_effect(|| Some(effect()), ())
647}
648
649/// useAsyncEffect hook that runs only once (on mount)
650///
651/// This is a convenience function for async effects that should run only once when
652/// the component mounts. It's equivalent to calling `use_async_effect(effect, ())`.
653///
654/// # Examples
655///
656/// ```rust,no_run
657/// # use reratui_hooks::effect::use_async_effect_once;
658/// use_async_effect_once(|| async {
659/// println!("This async effect runs only once");
660/// || async {} // Empty async cleanup
661/// });
662///
663/// // With async cleanup
664/// use_async_effect_once(|| async {
665/// println!("Setting up async subscription");
666/// move || async move {
667/// println!("Cleaning up async subscription");
668/// }
669/// });
670/// ```
671pub fn use_async_effect_once<F, Fut, C, CFut>(effect: F)
672where
673 F: FnOnce() -> Fut + Send + 'static,
674 Fut: std::future::Future<Output = C> + Send + 'static,
675 C: FnOnce() -> CFut + Send + 'static,
676 CFut: std::future::Future<Output = ()> + Send + 'static,
677{
678 use_async_effect(move || async move { Some(effect().await) }, ())
679}
680
681/// useAsyncEffect hook that runs on every render (no dependency tracking)
682///
683/// This is a convenience function for async effects that should run on every render.
684/// It's equivalent to calling `use_async_effect(effect, None)`.
685///
686/// # Examples
687///
688/// ```rust,no_run
689/// # use reratui_hooks::effect::use_async_effect_always;
690/// use_async_effect_always(|| async {
691/// println!("This async effect runs on every render");
692/// || async {} // Empty async cleanup
693/// });
694///
695/// // With async cleanup
696/// use_async_effect_always(|| async {
697/// println!("Setting up async resource");
698/// move || async move {
699/// println!("Cleaning up async resource");
700/// }
701/// });
702/// ```
703pub fn use_async_effect_always<F, Fut, C, CFut>(effect: F)
704where
705 F: FnOnce() -> Fut + Send + 'static,
706 Fut: std::future::Future<Output = C> + Send + 'static,
707 C: FnOnce() -> CFut + Send + 'static,
708 CFut: std::future::Future<Output = ()> + Send + 'static,
709{
710 use_async_effect::<(), _, _, C, CFut>(move || async move { Some(effect().await) }, None)
711}