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}