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}