pub fn effect(f: impl Fn() + 'static) -> EffectHandleExpand description
Creates an effect in the current thread’s default reactor.
The effect is scheduled immediately and then re-scheduled whenever one of its dependencies
changes. Effects never run inline with the write that triggered them: they are flushed on
the runtime’s microtask queue, so consecutive writes within one task coalesce into a single
run. Use effect_with to route runs to a lane of your own instead.
A queued run first verifies its inputs: when only equality-suppressed memo updates occurred upstream, the run is skipped without executing the body. If an upstream computation panics during that verification, the panic propagates out of the flush and the effect re-marks itself dirty and re-queues, so it recovers once the underlying cause is fixed.
Each run is an ownership scope: crate::on_cleanup callbacks and nested effects
registered during a run are disposed before the next run, and again when the effect itself
is disposed.
§Panics
Panics — in every build, release included — when the effect runs more than 100 times
within a single drain: the outermost flush and everything nested inside it, so a re-entrant
Reactor::flush_now cannot reset the count. That indicates a divergent feedback loop: the
effect writes state it (transitively) depends on with a value that never converges. The panic
message names the effect’s creation site. Convergent feedback (for example clamping, where the
rewritten value is suppressed by the signal’s equality check) is legal and settles well below
the limit.
The guard is deliberately not scoped to debug_assertions. The alternative to panicking in a
release build is not that the application carries on — it is a flush that never returns, with
no panic, no log and nothing for a user to report.
Both behaviors above describe an effect with no enclosing error boundary. Under a
crate::scope_catch, any panic from this effect — from its body or from dependency
verification — is instead delivered to that boundary’s handler, and the effect is disposed
rather than re-queued.
§Examples
use std::cell::RefCell;
use std::rc::Rc;
use adaptite::{effect, signal};
use runite::{queue_macrotask, run};
let seen = Rc::new(RefCell::new(Vec::new()));
queue_macrotask({
let seen = Rc::clone(&seen);
move || {
let value = signal(1);
effect({
let seen = Rc::clone(&seen);
let value = value.clone();
move || seen.borrow_mut().push(value.get())
})
.leak();
// Coalesces with the initial run: the effect observes only the final value.
value.set(2);
}
});
run();
assert_eq!(*seen.borrow(), [2]);