leptos_reactive/effect.rs
1use crate::{node::NodeId, with_runtime, Disposer, Runtime, SignalDispose};
2use cfg_if::cfg_if;
3use std::{any::Any, cell::RefCell, marker::PhantomData, rc::Rc};
4
5/// Effects run a certain chunk of code whenever the signals they depend on change.
6/// `create_effect` queues the given function to run once, tracks its dependence
7/// on any signal values read within it, and reruns the function whenever the value
8/// of a dependency changes.
9///
10/// Effects are intended to run *side-effects* of the system, not to synchronize state
11/// *within* the system. In other words: don't write to signals within effects, unless
12/// you’re coordinating with some other non-reactive side effect.
13/// (If you need to define a signal that depends on the value of other signals, use a
14/// derived signal or [`create_memo`](crate::create_memo)).
15///
16/// This first run is queued for the next microtask, i.e., it runs after all other
17/// synchronous code has completed. In practical terms, this means that if you use
18/// `create_effect` in the body of the component, it will run *after* the view has been
19/// created and (presumably) mounted. (If you need an effect that runs immediately, use
20/// [`create_render_effect`].)
21///
22/// The effect function is called with an argument containing whatever value it returned
23/// the last time it ran. On the initial run, this is `None`.
24///
25/// By default, effects **do not run on the server**. This means you can call browser-specific
26/// APIs within the effect function without causing issues. If you need an effect to run on
27/// the server, use [`create_isomorphic_effect`].
28/// ```
29/// # use leptos_reactive::*;
30/// # use log::*;
31/// # let runtime = create_runtime();
32/// let (a, set_a) = create_signal(0);
33/// let (b, set_b) = create_signal(0);
34///
35/// // ✅ use effects to interact between reactive state and the outside world
36/// create_effect(move |_| {
37/// // immediately prints "Value: 0" and subscribes to `a`
38/// log::debug!("Value: {}", a.get());
39/// });
40///
41/// set_a.set(1);
42/// // ✅ because it's subscribed to `a`, the effect reruns and prints "Value: 1"
43///
44/// // ❌ don't use effects to synchronize state within the reactive system
45/// create_effect(move |_| {
46/// // this technically works but can cause unnecessary re-renders
47/// // and easily lead to problems like infinite loops
48/// set_b.set(a.get() + 1);
49/// });
50/// # if !cfg!(feature = "ssr") {
51/// # assert_eq!(b.get(), 2);
52/// # }
53/// # runtime.dispose();
54/// ```
55#[cfg_attr(
56 any(debug_assertions, feature="ssr"),
57 instrument(
58 level = "trace",
59 skip_all,
60 fields(
61 ty = %std::any::type_name::<T>()
62 )
63 )
64)]
65#[track_caller]
66#[inline(always)]
67pub fn create_effect<T>(f: impl Fn(Option<T>) -> T + 'static) -> Effect<T>
68where
69 T: 'static,
70{
71 cfg_if! {
72 if #[cfg(not(feature = "ssr"))] {
73 use crate::{Owner, queue_microtask, with_owner};
74
75 let runtime = Runtime::current();
76 let owner = Owner::current();
77 let id = runtime.create_effect(f);
78
79 queue_microtask(move || {
80 with_owner(owner.unwrap(), move || {
81 _ = with_runtime( |runtime| {
82 runtime.update_if_necessary(id);
83 });
84 });
85 });
86
87 Effect { id, ty: PhantomData }
88 } else {
89 // clear warnings
90 _ = f;
91 Effect { id: Default::default(), ty: PhantomData }
92 }
93 }
94}
95
96impl<T> Effect<T>
97where
98 T: 'static,
99{
100 /// Effects run a certain chunk of code whenever the signals they depend on change.
101 /// `create_effect` immediately runs the given function once, tracks its dependence
102 /// on any signal values read within it, and reruns the function whenever the value
103 /// of a dependency changes.
104 ///
105 /// Effects are intended to run *side-effects* of the system, not to synchronize state
106 /// *within* the system. In other words: don't write to signals within effects.
107 /// (If you need to define a signal that depends on the value of other signals, use a
108 /// derived signal or [`create_memo`](crate::create_memo)).
109 ///
110 /// The effect function is called with an argument containing whatever value it returned
111 /// the last time it ran. On the initial run, this is `None`.
112 ///
113 /// By default, effects **do not run on the server**. This means you can call browser-specific
114 /// APIs within the effect function without causing issues. If you need an effect to run on
115 /// the server, use [`create_isomorphic_effect`].
116 /// ```
117 /// # use leptos_reactive::*;
118 /// # use log::*;
119 /// # let runtime = create_runtime();
120 /// let a = RwSignal::new(0);
121 /// let b = RwSignal::new(0);
122 ///
123 /// // ✅ use effects to interact between reactive state and the outside world
124 /// Effect::new(move |_| {
125 /// // immediately prints "Value: 0" and subscribes to `a`
126 /// log::debug!("Value: {}", a.get());
127 /// });
128 ///
129 /// a.set(1);
130 /// // ✅ because it's subscribed to `a`, the effect reruns and prints "Value: 1"
131 ///
132 /// // ❌ don't use effects to synchronize state within the reactive system
133 /// Effect::new(move |_| {
134 /// // this technically works but can cause unnecessary re-renders
135 /// // and easily lead to problems like infinite loops
136 /// b.set(a.get() + 1);
137 /// });
138 /// # if !cfg!(feature = "ssr") {
139 /// # assert_eq!(b.get(), 2);
140 /// # }
141 /// # runtime.dispose();
142 /// ```
143 #[track_caller]
144 #[inline(always)]
145 pub fn new(f: impl Fn(Option<T>) -> T + 'static) -> Self {
146 create_effect(f)
147 }
148
149 /// Creates an effect; unlike effects created by [`create_effect`], isomorphic effects will run on
150 /// the server as well as the client.
151 /// ```
152 /// # use leptos_reactive::*;
153 /// # use log::*;
154 /// # let runtime = create_runtime();
155 /// let a = RwSignal::new(0);
156 /// let b = RwSignal::new(0);
157 ///
158 /// // ✅ use effects to interact between reactive state and the outside world
159 /// Effect::new_isomorphic(move |_| {
160 /// // immediately prints "Value: 0" and subscribes to `a`
161 /// log::debug!("Value: {}", a.get());
162 /// });
163 ///
164 /// a.set(1);
165 /// // ✅ because it's subscribed to `a`, the effect reruns and prints "Value: 1"
166 ///
167 /// // ❌ don't use effects to synchronize state within the reactive system
168 /// Effect::new_isomorphic(move |_| {
169 /// // this technically works but can cause unnecessary re-renders
170 /// // and easily lead to problems like infinite loops
171 /// b.set(a.get() + 1);
172 /// });
173 /// # assert_eq!(b.get(), 2);
174 /// # runtime.dispose();
175 #[track_caller]
176 #[inline(always)]
177 pub fn new_isomorphic(f: impl Fn(Option<T>) -> T + 'static) -> Self {
178 create_isomorphic_effect(f)
179 }
180
181 /// Applies the given closure to the most recent value of the effect.
182 ///
183 /// Because effect functions can return values, each time an effect runs it
184 /// consumes its previous value. This allows an effect to store additional state
185 /// (like a DOM node, a timeout handle, or a type that implements `Drop`) and
186 /// keep it alive across multiple runs.
187 ///
188 /// This method allows access to the effect’s value outside the effect function.
189 /// The next time a signal change causes the effect to run, it will receive the
190 /// mutated value.
191 pub fn with_value_mut<U>(
192 &self,
193 f: impl FnOnce(&mut Option<T>) -> U,
194 ) -> Option<U> {
195 with_runtime(|runtime| {
196 let nodes = runtime.nodes.borrow();
197 let node = nodes.get(self.id)?;
198 let value = node.value.clone()?;
199 let mut value = value.borrow_mut();
200 let value = value.downcast_mut()?;
201 Some(f(value))
202 })
203 .ok()
204 .flatten()
205 }
206}
207
208/// Creates an effect; unlike effects created by [`create_effect`], isomorphic effects will run on
209/// the server as well as the client.
210/// ```
211/// # use leptos_reactive::*;
212/// # use log::*;
213/// # let runtime = create_runtime();
214/// let (a, set_a) = create_signal(0);
215/// let (b, set_b) = create_signal(0);
216///
217/// // ✅ use effects to interact between reactive state and the outside world
218/// create_isomorphic_effect(move |_| {
219/// // immediately prints "Value: 0" and subscribes to `a`
220/// log::debug!("Value: {}", a.get());
221/// });
222///
223/// set_a.set(1);
224/// // ✅ because it's subscribed to `a`, the effect reruns and prints "Value: 1"
225///
226/// // ❌ don't use effects to synchronize state within the reactive system
227/// create_isomorphic_effect(move |_| {
228/// // this technically works but can cause unnecessary re-renders
229/// // and easily lead to problems like infinite loops
230/// set_b.set(a.get() + 1);
231/// });
232/// # assert_eq!(b.get(), 2);
233/// # runtime.dispose();
234#[cfg_attr(
235 any(debug_assertions, feature="ssr"),
236 instrument(
237 level = "trace",
238 skip_all,
239 fields(
240 ty = %std::any::type_name::<T>()
241 )
242 )
243)]
244#[track_caller]
245#[inline(always)]
246pub fn create_isomorphic_effect<T>(
247 f: impl Fn(Option<T>) -> T + 'static,
248) -> Effect<T>
249where
250 T: 'static,
251{
252 let runtime = Runtime::current();
253 let id = runtime.create_effect(f);
254 //crate::macros::debug_warn!("creating effect {e:?}");
255 _ = with_runtime(|runtime| {
256 runtime.update_if_necessary(id);
257 });
258 Effect {
259 id,
260 ty: PhantomData,
261 }
262}
263
264/// Creates an effect exactly like [`create_effect`], but runs immediately rather
265/// than being queued until the end of the current microtask. This is mostly used
266/// inside the renderer but is available for use cases in which scheduling the effect
267/// for the next tick is not optimal.
268#[cfg_attr(
269 any(debug_assertions, feature="ssr"),
270 instrument(
271 level = "trace",
272 skip_all,
273 fields(
274 ty = %std::any::type_name::<T>()
275 )
276 )
277)]
278#[inline(always)]
279pub fn create_render_effect<T>(
280 f: impl Fn(Option<T>) -> T + 'static,
281) -> Effect<T>
282where
283 T: 'static,
284{
285 cfg_if! {
286 if #[cfg(not(feature = "ssr"))] {
287 let runtime = Runtime::current();
288 let id = runtime.create_effect(f);
289 _ = with_runtime( |runtime| {
290 runtime.update_if_necessary(id);
291 });
292 Effect { id, ty: PhantomData }
293 } else {
294 // clear warnings
295 _ = f;
296 Effect { id: Default::default(), ty: PhantomData }
297 }
298 }
299}
300
301/// A handle to an effect, can be used to explicitly dispose of the effect.
302#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
303pub struct Effect<T> {
304 pub(crate) id: NodeId,
305 ty: PhantomData<T>,
306}
307
308impl<T> From<Effect<T>> for Disposer {
309 fn from(effect: Effect<T>) -> Self {
310 Disposer(effect.id)
311 }
312}
313
314impl<T> SignalDispose for Effect<T> {
315 fn dispose(self) {
316 drop(Disposer::from(self));
317 }
318}
319
320pub(crate) struct EffectState<T, F>
321where
322 T: 'static,
323 F: Fn(Option<T>) -> T,
324{
325 pub(crate) f: F,
326 pub(crate) ty: PhantomData<T>,
327 #[cfg(any(debug_assertions, feature = "ssr"))]
328 pub(crate) defined_at: &'static std::panic::Location<'static>,
329}
330
331pub(crate) trait AnyComputation {
332 fn run(&self, value: Rc<RefCell<dyn Any>>) -> bool;
333}
334
335impl<T, F> AnyComputation for EffectState<T, F>
336where
337 T: 'static,
338 F: Fn(Option<T>) -> T,
339{
340 #[cfg_attr(
341 any(debug_assertions, feature = "ssr"),
342 instrument(
343 name = "Effect::run()",
344 level = "trace",
345 skip_all,
346 fields(
347 defined_at = %self.defined_at,
348 ty = %std::any::type_name::<T>()
349 )
350 )
351 )]
352 fn run(&self, value: Rc<RefCell<dyn Any>>) -> bool {
353 // we defensively take and release the BorrowMut twice here
354 // in case a change during the effect running schedules a rerun
355 // ideally this should never happen, but this guards against panic
356 let curr_value = {
357 // downcast value
358 let mut value = value.borrow_mut();
359 let value = value
360 .downcast_mut::<Option<T>>()
361 .expect("to downcast effect value");
362 value.take()
363 };
364
365 // run the effect
366 let new_value = (self.f)(curr_value);
367
368 // set new value
369 let mut value = value.borrow_mut();
370 let value = value
371 .downcast_mut::<Option<T>>()
372 .expect("to downcast effect value");
373 *value = Some(new_value);
374
375 true
376 }
377}