use super::owner::{child_scope, register_effect};
use super::runtime::{EffectId, run_effect_by_id, with_runtime};
pub fn create_effect<F>(f: F)
where
F: FnMut() + 'static,
{
create_effect_id(f);
}
pub(crate) fn create_effect_id<F>(f: F) -> EffectId
where
F: FnMut() + 'static,
{
spawn_effect(f, false)
}
pub(crate) fn create_memo_effect<F>(f: F)
where
F: FnMut() + 'static,
{
spawn_effect(f, true);
}
fn spawn_effect<F>(f: F, memo: bool) -> EffectId
where
F: FnMut() + 'static,
{
let scope = child_scope();
let id = with_runtime(|rt| rt.allocate_effect(Box::new(f), scope, memo));
run_effect_by_id(id);
register_effect(id);
id
}
#[cfg(test)]
mod tests {
use super::super::signal::create_signal;
use super::*;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
#[test]
fn an_unowned_effect_keeps_running() {
let signal = create_signal(0);
let ran = Arc::new(AtomicBool::new(false));
let ran_clone = ran.clone();
create_effect(move || {
let _ = signal.get();
ran_clone.store(true, Ordering::SeqCst);
});
assert!(ran.load(Ordering::SeqCst), "it runs once on creation");
ran.store(false, Ordering::SeqCst);
signal.set(1);
assert!(ran.load(Ordering::SeqCst));
}
#[test]
fn test_write_inside_effect_triggers_other_effects() {
use std::cell::Cell;
use std::rc::Rc;
let source = create_signal(0);
let intermediate = create_signal(0);
let observed = Rc::new(Cell::new(-1));
create_effect(move || {
intermediate.set(source.get() + 1);
});
let observed_b = observed.clone();
create_effect(move || {
observed_b.set(intermediate.get());
});
assert_eq!(observed.get(), 1, "initial chain should have run");
source.set(10);
assert_eq!(
observed.get(),
11,
"write inside effect A must re-run effect B"
);
}
#[test]
fn test_signal_created_inside_effect_is_reactive() {
use crate::reactive::owner::{current_owner, with_owner};
use std::cell::{Cell, RefCell};
use std::rc::Rc;
let trigger = create_signal(0);
let created: Rc<RefCell<Option<crate::reactive::RwSignal<i32>>>> =
Rc::new(RefCell::new(None));
let created_in_effect = created.clone();
with_owner(|| {
let outer = current_owner().unwrap();
create_effect(move || {
let t = trigger.get();
if created_in_effect.borrow().is_none() {
*created_in_effect.borrow_mut() = Some(outer.run(|| create_signal(t)));
}
});
});
trigger.set(1);
trigger.set(2);
let inner = created.borrow().expect("signal created inside effect");
let observed = Rc::new(Cell::new(-1));
let observed_c = observed.clone();
create_effect(move || {
observed_c.set(inner.get());
});
inner.set(42);
assert_eq!(
observed.get(),
42,
"signal created inside an effect must notify its subscribers"
);
}
#[test]
fn a_cleanup_registered_by_a_run_runs_before_the_next() {
use crate::reactive::owner::{dispose_owner_now, on_cleanup, with_owner};
use std::cell::RefCell;
use std::rc::Rc;
let trigger = create_signal(0);
let log = Rc::new(RefCell::new(Vec::new()));
let in_effect = log.clone();
let ((), scope) = with_owner(|| {
create_effect(move || {
let run = trigger.get();
in_effect.borrow_mut().push(format!("run {run}"));
let in_cleanup = in_effect.clone();
on_cleanup(move || in_cleanup.borrow_mut().push(format!("cleanup {run}")));
});
});
trigger.set(1);
trigger.set(2);
dispose_owner_now(scope);
assert_eq!(
*log.borrow(),
[
"run 0",
"cleanup 0",
"run 1",
"cleanup 1",
"run 2",
"cleanup 2"
]
);
}
#[test]
fn what_a_run_creates_is_disposed_by_the_next() {
use crate::reactive::Signal;
use std::cell::{Cell, RefCell};
use std::rc::Rc;
let trigger = create_signal(0);
let nested_source = create_signal(0);
let made: Rc<RefCell<Vec<Signal<i32>>>> = Rc::default();
let nested_runs = Rc::new(Cell::new(0));
let in_effect = made.clone();
let counted = nested_runs.clone();
create_effect(move || {
in_effect
.borrow_mut()
.push(create_signal(trigger.get()).read_only());
let counted = counted.clone();
create_effect(move || {
nested_source.get();
counted.set(counted.get() + 1);
});
});
let first = made.borrow()[0];
assert!(first.is_live());
assert_eq!(nested_runs.get(), 1);
trigger.set(1);
assert!(!first.is_live(), "the first run's signal went with it");
assert!(
made.borrow()[1].is_live(),
"the second run's is still there"
);
assert_eq!(
nested_runs.get(),
2,
"one nested effect, made by the new run"
);
nested_source.set(1);
assert_eq!(
nested_runs.get(),
3,
"only the newest nested effect runs; the first run's is gone"
);
}
#[test]
fn disposing_the_owner_disposes_the_last_run() {
use crate::reactive::Signal;
use crate::reactive::owner::{dispose_owner_now, with_owner};
use std::cell::Cell;
use std::rc::Rc;
let trigger = create_signal(0);
let made: Rc<Cell<Option<Signal<i32>>>> = Rc::default();
let in_effect = made.clone();
let ((), scope) = with_owner(|| {
create_effect(move || in_effect.set(Some(create_signal(trigger.get()).read_only())));
});
trigger.set(1);
let last = made.get().unwrap();
assert!(last.is_live());
dispose_owner_now(scope);
assert!(!last.is_live());
}
#[test]
fn the_opt_out_keeps_what_it_creates() {
use crate::reactive::Signal;
use crate::reactive::owner::{current_owner, dispose_owner_now, with_owner};
use std::cell::RefCell;
use std::rc::Rc;
let trigger = create_signal(0);
let made: Rc<RefCell<Vec<Signal<i32>>>> = Rc::default();
let in_effect = made.clone();
let ((), outer) = with_owner(|| {
let outer = current_owner().unwrap();
create_effect(move || {
let kept = outer.run(|| create_signal(trigger.get()));
in_effect.borrow_mut().push(kept.read_only());
});
});
trigger.set(1);
trigger.set(2);
assert!(made.borrow().iter().all(Signal::is_live));
dispose_owner_now(outer);
assert!(!made.borrow().iter().any(Signal::is_live));
}
#[test]
fn cleanups_run_children_first() {
use crate::reactive::owner::{on_cleanup, with_owner};
use std::cell::RefCell;
use std::rc::Rc;
let trigger = create_signal(0);
let log = Rc::new(RefCell::new(Vec::new()));
let in_effect = log.clone();
create_effect(move || {
if trigger.get() > 0 {
return;
}
let pushed = |what: &'static str| {
let log = in_effect.clone();
move || log.borrow_mut().push(what)
};
on_cleanup(pushed("first"));
with_owner(|| on_cleanup(pushed("child")));
let made = create_signal("signal still live");
let log = in_effect.clone();
on_cleanup(move || log.borrow_mut().push(made.get()));
on_cleanup(pushed("second"));
});
assert!(log.borrow().is_empty());
trigger.set(1);
assert_eq!(
*log.borrow(),
["child", "second", "signal still live", "first"]
);
}
}