1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
use crate::runtime::{with_runtime, RuntimeId};
use crate::{debug_warn, Runtime, Scope, ScopeProperty};
use cfg_if::cfg_if;
use std::cell::RefCell;
use std::fmt::Debug;

/// Effects run a certain chunk of code whenever the signals they depend on change.
/// `create_effect` immediately runs the given function once, tracks its dependence
/// on any signal values read within it, and reruns the function whenever the value
/// of a dependency changes.
///
/// Effects are intended to run *side-effects* of the system, not to synchronize state
/// *within* the system. In other words: don't write to signals within effects.
/// (If you need to define a signal that depends on the value of other signals, use a
/// derived signal or [create_memo](crate::create_memo)).
///
/// The effect function is called with an argument containing whatever value it returned
/// the last time it ran. On the initial run, this is `None`.
///
/// By default, effects **do not run on the server**. This means you can call browser-specific
/// APIs within the effect function without causing issues. If you need an effect to run on
/// the server, use [create_isomorphic_effect].
/// ```
/// # use leptos_reactive::*;
/// # use log::*;
/// # create_scope(create_runtime(), |cx| {
/// let (a, set_a) = create_signal(cx, 0);
/// let (b, set_b) = create_signal(cx, 0);
///
/// // ✅ use effects to interact between reactive state and the outside world
/// create_effect(cx, move |_| {
///   // immediately prints "Value: 0" and subscribes to `a`
///   log::debug!("Value: {}", a());
/// });
///
/// set_a(1);
/// // ✅ because it's subscribed to `a`, the effect reruns and prints "Value: 1"
///
/// // ❌ don't use effects to synchronize state within the reactive system
/// create_effect(cx, move |_| {
///   // this technically works but can cause unnecessary re-renders
///   // and easily lead to problems like infinite loops
///   set_b(a() + 1);
/// });
/// # if !cfg!(feature = "ssr") {
/// # assert_eq!(b(), 2);
/// # }
/// # }).dispose();
/// ```
pub fn create_effect<T>(cx: Scope, f: impl Fn(Option<T>) -> T + 'static)
where
    T: Debug + 'static,
{
    cfg_if! {
        if #[cfg(not(feature = "ssr"))] {
            create_isomorphic_effect(cx, f);
        } else {
            // clear warnings
            _ = cx;
            _ = f;
        }
    }
}

/// Creates an effect; unlike effects created by [create_effect], isomorphic effects will run on
/// the server as well as the client.
/// ```
/// # use leptos_reactive::*;
/// # use log::*;
/// # create_scope(create_runtime(), |cx| {
/// let (a, set_a) = create_signal(cx, 0);
/// let (b, set_b) = create_signal(cx, 0);
///
/// // ✅ use effects to interact between reactive state and the outside world
/// create_isomorphic_effect(cx, move |_| {
///   // immediately prints "Value: 0" and subscribes to `a`
///   log::debug!("Value: {}", a());
/// });
///
/// set_a(1);
/// // ✅ because it's subscribed to `a`, the effect reruns and prints "Value: 1"
///
/// // ❌ don't use effects to synchronize state within the reactive system
/// create_isomorphic_effect(cx, move |_| {
///   // this technically works but can cause unnecessary re-renders
///   // and easily lead to problems like infinite loops
///   set_b(a() + 1);
/// });
/// # assert_eq!(b(), 2);
/// # }).dispose();
pub fn create_isomorphic_effect<T>(cx: Scope, f: impl Fn(Option<T>) -> T + 'static)
where
    T: Debug + 'static,
{
    let e = cx.runtime.create_effect(f);
    cx.with_scope_property(|prop| prop.push(ScopeProperty::Effect(e)))
}

#[doc(hidden)]
pub fn create_render_effect<T>(cx: Scope, f: impl Fn(Option<T>) -> T + 'static)
where
    T: Debug + 'static,
{
    create_effect(cx, f);
}

slotmap::new_key_type! {
    /// Unique ID assigned to an [Effect](crate::Effect).
    pub(crate) struct EffectId;
}

pub(crate) struct Effect<T, F>
where
    T: 'static,
    F: Fn(Option<T>) -> T,
{
    pub(crate) f: F,
    pub(crate) value: RefCell<Option<T>>,
}

pub(crate) trait AnyEffect {
    fn run(&self, id: EffectId, runtime: RuntimeId);
}

impl<T, F> AnyEffect for Effect<T, F>
where
    T: 'static,
    F: Fn(Option<T>) -> T,
{
    fn run(&self, id: EffectId, runtime: RuntimeId) {
        with_runtime(runtime, |runtime| {
            // clear previous dependencies
            id.cleanup(runtime);

            // set this as the current observer
            let prev_observer = runtime.observer.take();
            runtime.observer.set(Some(id));

            // run the effect
            let value = self.value.take();
            let new_value = (self.f)(value);
            *self.value.borrow_mut() = Some(new_value);

            // restore the previous observer
            runtime.observer.set(prev_observer);
        })
    }
}

impl EffectId {
    pub(crate) fn run<T>(&self, runtime_id: RuntimeId) {
        with_runtime(runtime_id, |runtime| {
            let effect = {
                let effects = runtime.effects.borrow();
                effects.get(*self).cloned()
            };
            if let Some(effect) = effect {
                effect.run(*self, runtime_id);
            } else {
                debug_warn!("[Effect] Trying to run an Effect that has been disposed. This is probably either a logic error in a component that creates and disposes of scopes, or a Resource resolving after its scope has been dropped without having been cleaned up.")
            }
        })
    }

    pub(crate) fn cleanup(&self, runtime: &Runtime) {
        let sources = runtime.effect_sources.borrow();
        if let Some(sources) = sources.get(*self) {
            let subs = runtime.signal_subscribers.borrow();
            for source in sources.borrow().iter() {
                if let Some(source) = subs.get(*source) {
                    source.borrow_mut().remove(self);
                }
            }
        }
    }
}