Skip to main content

freya_core/lifecycle/
effect.rs

1use std::{
2    cell::RefCell,
3    rc::Rc,
4};
5
6use generational_box::{
7    AnyStorage,
8    UnsyncStorage,
9};
10
11use crate::{
12    lifecycle::writable_utils::WritableUtils,
13    prelude::{
14        State,
15        TaskHandle,
16        spawn,
17        spawn_global,
18        use_hook,
19        use_reactive,
20    },
21    reactive_context::ReactiveContext,
22};
23
24/// Creates side effect callback that is rerun when reactive values read on it, change.
25///
26/// The usual way is to use `use_side_effect`, but you can use the several [`Effect`] methods like [`Effect::create`] inside `use_hook`
27/// or [`Effect::create_global`] inside `LaunchConfig::with_task`.
28pub struct Effect;
29
30impl Effect {
31    /// Runs the callback asynchronously in the current component.
32    /// Reruns when a reactive value read by the callback changes.
33    ///
34    /// Unlike [`Effect::create_sync`], the first run is not immediate.
35    ///
36    /// ```rust,no_run
37    /// # use freya::prelude::*;
38    /// # fn app() -> impl IntoElement {
39    /// let count = use_state(|| 0);
40    /// use_hook(move || {
41    ///     Effect::create(move || {
42    ///         println!("Count: {}", *count.read());
43    ///     })
44    /// });
45    /// rect()
46    /// # }
47    /// ```
48    pub fn create(mut callback: impl FnMut() + 'static) {
49        let (rx, rc) = ReactiveContext::new_for_task();
50        spawn(async move {
51            loop {
52                ReactiveContext::run(rc.clone(), &mut callback);
53                rx.notified().await;
54            }
55        });
56    }
57
58    /// Runs immediately with generation `0`.
59    /// Then, on every change the callback reruns with an increased generation.
60    ///
61    /// ```rust,no_run
62    /// # use freya::prelude::*;
63    /// # fn app() -> impl IntoElement {
64    /// let count = use_state(|| 0);
65    /// use_hook(move || {
66    ///     Effect::create_sync_with_gen(move |generation| {
67    ///         println!("Run {generation}: {}", *count.read());
68    ///     })
69    /// });
70    /// rect()
71    /// # }
72    /// ```
73    pub fn create_sync_with_gen(mut callback: impl FnMut(usize) + 'static) {
74        let (rx, rc) = ReactiveContext::new_for_task();
75        ReactiveContext::run(rc.clone(), || callback(0));
76        spawn(async move {
77            let mut current_gen = 1;
78            loop {
79                rx.notified().await;
80                ReactiveContext::run(rc.clone(), || callback(current_gen));
81                current_gen += 1;
82            }
83        });
84    }
85
86    /// Runs immediately and then on every change the callback reruns with an increased generation.
87    ///
88    /// ```rust,no_run
89    /// # use freya::prelude::*;
90    /// # fn app() -> impl IntoElement {
91    /// let count = use_state(|| 0);
92    /// use_hook(move || {
93    ///     Effect::create_sync(move || {
94    ///         println!("Count: {}", *count.read());
95    ///     })
96    /// });
97    /// rect()
98    /// # }
99    /// ```
100    pub fn create_sync(mut callback: impl FnMut() + 'static) {
101        let (rx, rc) = ReactiveContext::new_for_task();
102        ReactiveContext::run(rc.clone(), &mut callback);
103        spawn(async move {
104            loop {
105                rx.notified().await;
106                ReactiveContext::run(rc.clone(), &mut callback);
107            }
108        });
109    }
110
111    /// Spawns each callback run in a separate task, including the first run.
112    /// Reruns when a reactive value read by the callback changes.
113    ///
114    /// Unlike [`Effect::create`], the callback does not run in the task
115    /// listening for changes.
116    ///
117    /// ```rust,no_run
118    /// # use freya::prelude::*;
119    /// # fn app() -> impl IntoElement {
120    /// let count = use_state(|| 0);
121    /// use_hook(move || {
122    ///     Effect::create_after(move || {
123    ///         println!("Count: {}", *count.read());
124    ///     })
125    /// });
126    /// rect()
127    /// # }
128    /// ```
129    pub fn create_after(callback: impl FnMut() + 'static) {
130        let (rx, rc) = ReactiveContext::new_for_task();
131        let callback = Rc::new(RefCell::new(callback));
132        spawn(async move {
133            loop {
134                let callback = callback.clone();
135                let rc = rc.clone();
136                spawn(async move {
137                    ReactiveContext::run(rc, &mut *callback.borrow_mut());
138                });
139                rx.notified().await;
140            }
141        });
142    }
143
144    /// Computes the initial value immediately and returns it as [`State<T>`].
145    /// Reactive changes rerun the callback in a task and update the value.
146    ///
147    /// ```rust,no_run
148    /// # use freya::prelude::*;
149    /// # fn app() -> impl IntoElement {
150    /// let count = use_state(|| 1);
151    /// let doubled = use_hook(move || Effect::create_value(move || *count.read() * 2));
152    /// rect().child(format!("Doubled: {}", *doubled.read()))
153    /// # }
154    /// ```
155    pub fn create_value<T: 'static>(mut callback: impl FnMut() -> T + 'static) -> State<T> {
156        let (rx, rc) = ReactiveContext::new_for_task();
157        let mut state = State::create(ReactiveContext::run(rc.clone(), &mut callback));
158        spawn(async move {
159            let mut current_gen = 0;
160            loop {
161                if current_gen > 0 {
162                    state.set(ReactiveContext::run(rc.clone(), &mut callback));
163                }
164                rx.notified().await;
165                current_gen += 1;
166            }
167        });
168        state
169    }
170
171    /// Runs asynchronously and reruns when reactive values read by the callback change.
172    /// Lives independently of components and windows.
173    ///
174    /// Stops when the application exits or the returned [`TaskHandle`] is cancelled.
175    ///
176    /// You should usually call this inside `LaunchConfig::with_task`, not in components.
177    ///
178    /// ```rust,no_run
179    /// # use freya::prelude::*;
180    /// # fn main() {
181    /// let count = State::create_global(0);
182    /// launch(
183    ///     LaunchConfig::new()
184    ///         .with_exit_on_close(false)
185    ///         .with_task(move |_| async move {
186    ///             Effect::create_global(move || {
187    ///                 println!("Count: {}", *count.read());
188    ///             });
189    ///         }),
190    /// );
191    /// # }
192    /// ```
193    pub fn create_global(mut callback: impl FnMut() + 'static) -> TaskHandle {
194        let owner = UnsyncStorage::owner();
195        let (notification, context) = ReactiveContext::new_for_async(&owner);
196        spawn_global(async move {
197            let _owner = owner;
198            loop {
199                ReactiveContext::run(context.clone(), &mut callback);
200                notification.notified().await;
201            }
202        })
203    }
204}
205
206/// Hook for [`Effect::create`](Effect::create).
207/// Registers the effect once per component.
208///
209/// ```rust,no_run
210/// # use freya::prelude::*;
211/// # fn app() -> impl IntoElement {
212/// let count = use_state(|| 0);
213/// use_side_effect(move || println!("Count: {}", *count.read()));
214/// rect()
215/// # }
216/// ```
217pub fn use_side_effect(callback: impl FnMut() + 'static) {
218    use_hook(|| Effect::create(callback));
219}
220
221/// Hook for [`Effect::create_after`](Effect::create_after).
222/// Registers the effect once per component.
223///
224/// ```rust,no_run
225/// # use freya::prelude::*;
226/// # fn app() -> impl IntoElement {
227/// let count = use_state(|| 0);
228/// use_after_side_effect(move || println!("Count: {}", *count.read()));
229/// rect()
230/// # }
231/// ```
232pub fn use_after_side_effect(callback: impl FnMut() + 'static) {
233    use_hook(|| Effect::create_after(callback));
234}
235
236/// Hook for [`Effect::create_value`](Effect::create_value).
237/// Registers the effect once per component and returns its [`State<T>`].
238///
239/// ```rust,no_run
240/// # use freya::prelude::*;
241/// # fn app() -> impl IntoElement {
242/// let count = use_state(|| 1);
243/// let doubled = use_side_effect_value(move || *count.read() * 2);
244/// rect().child(format!("Doubled: {}", *doubled.read()))
245/// # }
246/// ```
247pub fn use_side_effect_value<T: 'static>(callback: impl FnMut() -> T + 'static) -> State<T> {
248    use_hook(|| Effect::create_value(callback))
249}
250
251/// Hook for [`Effect::create`](Effect::create) with reactive `deps`.
252/// Registers the effect once per component.
253///
254/// [`use_reactive`] compares `deps` on every render using [`PartialEq`].
255/// The callback receives the latest value when `deps` changes.
256///
257/// ```rust,no_run
258/// # use freya::prelude::*;
259/// #[derive(PartialEq)]
260/// struct Title(String);
261/// impl Component for Title {
262///     fn render(&self) -> impl IntoElement {
263///         use_side_effect_with_deps(&self.0, |title| println!("Title: {title}"));
264///         rect().child(self.0.clone())
265///     }
266/// }
267/// ```
268pub fn use_side_effect_with_deps<D: 'static + Clone + PartialEq>(
269    deps: &D,
270    mut callback: impl FnMut(&D) + 'static,
271) {
272    let deps = use_reactive(deps);
273    use_hook(move || {
274        Effect::create(move || {
275            let deps = deps.read();
276            callback(&deps)
277        })
278    });
279}