Skip to main content

repose_core/
effects.rs

1use std::cell::RefCell;
2use std::rc::Rc;
3
4#[derive(Clone)]
5pub struct Dispose(Rc<RefCell<Option<Box<dyn FnOnce()>>>>);
6
7impl Dispose {
8    pub fn new(f: impl FnOnce() + 'static) -> Self {
9        Self(Rc::new(RefCell::new(Some(Box::new(f)))))
10    }
11
12    pub(crate) fn ptr_eq(&self, other: &Self) -> bool {
13        Rc::ptr_eq(&self.0, &other.0)
14    }
15
16    pub fn run(&self) {
17        let function = {
18            let mut function = self.0.borrow_mut();
19            function.take()
20        };
21        if let Some(function) = function {
22            function();
23        }
24    }
25}
26
27/// Runs `f()` immediately and returns its `Dispose`, registering cleanup on
28/// the current scope when one exists. Like `scoped_effect`, this runs on every
29/// call - for mount-once semantics use `scoped_effect_once` or
30/// `disposable_effect`.
31pub fn effect<F>(f: F) -> Dispose
32where
33    F: FnOnce() -> Dispose + 'static,
34{
35    let d = f();
36
37    if let Some(scope) = crate::scope::current_scope() {
38        let d2 = d.clone();
39        scope.add_disposer(move || d2.run());
40    } else {
41        debug_assert!(
42            false,
43            "effect called without a current Scope; cleanup cannot be tracked"
44        );
45        log::error!("effect called without a current Scope; cleanup untracked");
46    }
47
48    d
49}
50
51/// Mount-once effect: runs `f` only the first time this call site composes.
52/// Later recompositions return the original `Dispose` without re-running setup.
53#[track_caller]
54pub fn effect_once(f: impl FnOnce() -> Dispose + 'static) -> Dispose {
55    let loc = std::panic::Location::caller();
56    effect_once_with_key(
57        format!("effect:{}:{}:{}", loc.file(), loc.line(), loc.column()),
58        f,
59    )
60}
61
62pub fn effect_once_with_key(
63    key: impl Into<String>,
64    f: impl FnOnce() -> Dispose + 'static,
65) -> Dispose {
66    let key = key.into();
67    let slot = crate::remember_with_key(key.clone(), || RefCell::new(None::<Dispose>));
68    let existing = {
69        let slot = slot.borrow();
70        slot.as_ref().cloned()
71    };
72    if let Some(existing) = existing {
73        return register_scope_owner(&key, &existing);
74    }
75
76    let disposer = f();
77    let old = {
78        let mut slot = slot.borrow_mut();
79        slot.replace(disposer.clone())
80    };
81    drop(old);
82    register_scope_owner(&key, &disposer)
83}
84
85fn register_scope_owner(key: &str, disposer: &Dispose) -> Dispose {
86    let Some(scope) = crate::scope::current_scope() else {
87        crate::runtime::register_keyed_disposer(key.to_string(), disposer.clone());
88        return disposer.clone();
89    };
90    let owner =
91        crate::runtime::scope_owner_token(crate::scope_cache::current_scope_key().as_deref());
92    let token_key = key.to_string();
93    let token_disposer = disposer.clone();
94    let token_owner = owner.clone();
95    if !crate::runtime::keyed_disposer_has_owner(&token_key, &token_disposer, &owner) {
96        crate::runtime::register_keyed_disposer_for_owner(
97            token_key.clone(),
98            token_disposer.clone(),
99            owner.clone(),
100        );
101        let registered = token_disposer.clone();
102        let scope_key = token_key.clone();
103        let scope_owner = owner.clone();
104        scope.add_disposer(move || {
105            crate::runtime::remove_keyed_disposer_for_owner(&scope_key, &registered, &scope_owner);
106        });
107    }
108    Dispose::new(move || {
109        crate::runtime::remove_keyed_disposer_for_owner(&token_key, &token_disposer, &token_owner);
110    })
111}
112/// Helper to register cleanup inside effect.
113pub fn on_unmount(f: impl FnOnce() + 'static) -> Dispose {
114    Dispose::new(f)
115}