Skip to main content

repose_core/
effects_ext.rs

1use crate::{Dispose, on_unmount, scoped_effect};
2use std::cell::RefCell;
3
4/// Callsite-key for branch-stable effect storage.
5macro_rules! effect_key {
6    ($prefix:literal) => {{
7        let loc = std::panic::Location::caller();
8        format!("{}:{}:{}:{}", $prefix, loc.file(), loc.line(), loc.column())
9    }};
10}
11
12/// cleanup on key change or unmount. Storage is callsite-keyed, so conditional
13/// branches above the call site cannot shift slots.
14#[track_caller]
15pub fn disposable_effect<K: PartialEq + Clone + 'static>(
16    key: K,
17    effect: impl FnOnce() -> Dispose + 'static,
18) {
19    disposable_effect_with_callsite(effect_key!("de"), key, effect);
20}
21
22/// runs on every recomposition
23pub fn side_effect(effect: impl Fn()) {
24    effect();
25}
26
27/// Internal implementation: keyed by a per-callsite id string.
28/// Runs the effect when `key` changes and runs the returned `Dispose` on key
29/// change or unmount (cancellable launched effect).
30pub fn launched_effect_internal<K: PartialEq + Clone + 'static>(
31    callsite: &'static str,
32    key: K,
33    effect: impl FnOnce() -> Dispose + 'static,
34) {
35    disposable_effect_with_callsite(format!("launched:{callsite}"), key, effect);
36}
37
38fn disposable_effect_with_callsite<K: PartialEq + Clone + 'static>(
39    callsite: String,
40    key: K,
41    effect: impl FnOnce() -> Dispose + 'static,
42) {
43    let last_key = crate::remember_with_key(format!("{callsite}:last"), || RefCell::new(None::<K>));
44    let cleanup_key = format!("{callsite}:cleanup");
45    let cleanup_slot =
46        crate::remember_with_key(cleanup_key.clone(), || RefCell::new(None::<Dispose>));
47    let owner =
48        crate::runtime::scope_owner_token(crate::scope_cache::current_scope_key().as_deref());
49    let installed = crate::remember_with_key(format!("{callsite}:installed:{owner}"), || {
50        RefCell::new(false)
51    });
52
53    if !*installed.borrow() {
54        *installed.borrow_mut() = true;
55        let cleanup_slot = cleanup_slot.clone();
56        let cleanup_key = cleanup_key.clone();
57        let cleanup_owner = owner.clone();
58        scoped_effect(move || {
59            on_unmount(move || {
60                let disposer = cleanup_slot.borrow().as_ref().cloned();
61                if let Some(disposer) = disposer.as_ref() {
62                    crate::runtime::remove_keyed_disposer_for_owner(
63                        &cleanup_key,
64                        disposer,
65                        &cleanup_owner,
66                    );
67                }
68                let (_, removed) =
69                    crate::runtime::release_keyed_owner(&cleanup_key, &cleanup_owner);
70                drop(removed);
71            })
72        });
73    }
74
75    let changed = last_key.borrow().as_ref() != Some(&key);
76    if changed {
77        let old_key = {
78            let mut last = last_key.borrow_mut();
79            last.replace(key.clone())
80        };
81        drop(old_key);
82        let previous = {
83            let mut slot = cleanup_slot.borrow_mut();
84            slot.take()
85        };
86        if let Some(previous) = previous {
87            crate::runtime::remove_keyed_disposer_for_owner(&cleanup_key, &previous, &owner);
88        }
89        let d = effect();
90        let old = {
91            let mut slot = cleanup_slot.borrow_mut();
92            slot.replace(d.clone())
93        };
94        drop(old);
95        crate::runtime::register_keyed_disposer_for_owner(cleanup_key.clone(), d, owner.clone());
96    }
97}
98
99/// Fire-and-forget launched effect: runs `effect` on key change with no
100/// cleanup on unmount. Prefer `launched_effect!` (cancellable) unless the work
101/// is intentionally uncancellable.
102pub fn launched_effect_uncancelled_internal<K: PartialEq + Clone + 'static>(
103    callsite: &'static str,
104    key: K,
105    effect: impl FnOnce() + 'static,
106) {
107    let last_key =
108        crate::remember_with_key(format!("launched:{callsite}"), || RefCell::new(None::<K>));
109
110    let changed = last_key.borrow().as_ref() != Some(&key);
111    if changed {
112        let old = {
113            let mut last = last_key.borrow_mut();
114            last.replace(key.clone())
115        };
116        drop(old);
117        effect();
118    }
119}
120
121#[macro_export] // Should probably move this to macros (might want to move the above part too?)
122macro_rules! launched_effect {
123    ($key:expr, $effect:expr) => {
124        $crate::effects_ext::launched_effect_internal(
125            concat!(module_path!(), ":", line!(), ":", column!()),
126            $key,
127            $effect,
128        )
129    };
130}
131
132#[macro_export]
133macro_rules! launched_effect_uncancelled {
134    ($key:expr, $effect:expr) => {
135        $crate::effects_ext::launched_effect_uncancelled_internal(
136            concat!(module_path!(), ":", line!(), ":", column!()),
137            $key,
138            $effect,
139        )
140    };
141}