cranpose_core/
disposable_effect.rs1use crate::{Key, caller_location_key, effect_key, with_current_composer};
2
3#[derive(Default)]
4pub(crate) struct DisposableEffectState {
5 key: Option<effect_key::EffectKey>,
6 cleanup: Option<Box<dyn FnOnce()>>,
7}
8
9impl DisposableEffectState {
10 fn should_run<K: PartialEq + 'static>(&self, keys: &K) -> bool {
11 self.key.as_ref().is_none_or(|current| !current.holds(keys))
12 }
13
14 fn set_key(&mut self, key: effect_key::EffectKey) {
15 self.key = Some(key);
16 }
17
18 fn set_cleanup(&mut self, cleanup: Option<Box<dyn FnOnce()>>) {
19 self.cleanup = cleanup;
20 }
21
22 fn run_cleanup(&mut self) {
23 if let Some(cleanup) = self.cleanup.take() {
24 cleanup();
25 }
26 }
27}
28
29impl Drop for DisposableEffectState {
30 fn drop(&mut self) {
31 self.run_cleanup();
32 }
33}
34
35#[derive(Clone, Copy, Debug, Default)]
38pub struct DisposableEffectScope;
39
40#[derive(Default)]
43pub struct DisposableEffectResult {
44 cleanup: Option<Box<dyn FnOnce()>>,
45}
46
47impl DisposableEffectScope {
48 pub fn on_dispose(&self, cleanup: impl FnOnce() + 'static) -> DisposableEffectResult {
52 DisposableEffectResult::new(cleanup)
53 }
54}
55
56impl DisposableEffectResult {
57 pub fn new(cleanup: impl FnOnce() + 'static) -> Self {
59 Self {
60 cleanup: Some(Box::new(cleanup)),
61 }
62 }
63
64 fn into_cleanup(self) -> Option<Box<dyn FnOnce()>> {
65 self.cleanup
66 }
67}
68
69#[expect(non_snake_case, reason = "names follow Compose's casing")]
72pub fn SideEffect(effect: impl FnOnce() + 'static) {
73 with_current_composer(|composer| composer.register_side_effect(effect));
74}
75
76#[doc(hidden)]
79pub fn __disposable_effect_impl<K, F>(group_key: Key, keys: K, effect: F)
80where
81 K: PartialEq + 'static,
82 F: FnOnce(DisposableEffectScope) -> DisposableEffectResult + 'static,
83{
84 with_current_composer(|composer| {
85 let state = composer.remember_effect::<DisposableEffectState>(group_key);
86 if state.with(|state| state.should_run(&keys)) {
87 let key = effect_key::EffectKey::new(keys);
88 state.update(|state| {
89 state.run_cleanup();
90 state.set_key(key);
91 });
92 let mut effect_opt = Some(effect);
93 composer.register_side_effect(move || {
94 if let Some(effect) = effect_opt.take() {
95 let result = effect(DisposableEffectScope);
96 state.update(|state| state.set_cleanup(result.into_cleanup()));
97 }
98 });
99 }
100 });
101}
102
103#[expect(non_snake_case, reason = "names follow Compose's casing")]
111#[track_caller]
112pub fn DisposableEffect<K, F>(keys: K, effect: F)
113where
114 K: PartialEq + 'static,
115 F: FnOnce(DisposableEffectScope) -> DisposableEffectResult + 'static,
116{
117 __disposable_effect_impl(caller_location_key(), keys, effect);
118}
119
120#[macro_export]
125macro_rules! clone_captures {
126 ($($alias:ident $(= $value:expr)?),+ $(,)?; $body:expr) => {{
127 $(let $alias = $crate::clone_captures!(@clone $alias $(= $value)?);)+
128 $body
129 }};
130 (@clone $alias:ident = $value:expr) => {
131 ($value).clone()
132 };
133 (@clone $alias:ident) => {
134 $alias.clone()
135 };
136}