Skip to main content

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}