From 42a58855a08620398cb727b1083b11adfea2c9fa Mon Sep 17 00:00:00 2001 From: Greg Johnston Date: Mon, 20 Mar 2023 08:29:18 -0400 Subject: [PATCH] feat: add `Scope::batch()` (#711) --- leptos_reactive/src/runtime.rs | 13 +++++++--- leptos_reactive/src/scope.rs | 19 ++++++++++++++ leptos_reactive/src/signal.rs | 2 +- leptos_reactive/tests/effect.rs | 46 +++++++++++++++++++++++++++++++-- 4 files changed, 73 insertions(+), 7 deletions(-) diff --git a/leptos_reactive/src/runtime.rs b/leptos_reactive/src/runtime.rs index 6e187de..97261e2 100644 --- a/leptos_reactive/src/runtime.rs +++ b/leptos_reactive/src/runtime.rs @@ -62,6 +62,7 @@ pub(crate) struct Runtime { RefCell>>>, pub pending_effects: RefCell>, pub resources: RefCell>, + pub batching: Cell, } // This core Runtime impl block handles all the work of marking and updating @@ -248,13 +249,17 @@ impl Runtime { pub(crate) fn run_effects(runtime_id: RuntimeId) { _ = with_runtime(runtime_id, |runtime| { - let effects = runtime.pending_effects.take(); - for effect_id in effects { - runtime.update_if_necessary(effect_id); - } + runtime.run_your_effects(); }); } + pub(crate) fn run_your_effects(&self) { + let effects = self.pending_effects.take(); + for effect_id in effects { + self.update_if_necessary(effect_id); + } + } + pub(crate) fn dispose_node(&self, node: NodeId) { self.node_sources.borrow_mut().remove(node); self.node_subscribers.borrow_mut().remove(node); diff --git a/leptos_reactive/src/scope.rs b/leptos_reactive/src/scope.rs index efc6798..3772f89 100644 --- a/leptos_reactive/src/scope.rs +++ b/leptos_reactive/src/scope.rs @@ -442,6 +442,25 @@ impl Scope { .ok() .flatten() } + + /// Batches any reactive updates, preventing effects from running until the whole + /// function has run. This allows you to prevent rerunning effects if multiple + /// signal updates might cause the same effect to run. + /// + /// # Panics + /// Panics if the runtime this scope belongs to has already been disposed. + pub fn batch(&self, f: impl FnOnce() -> T) -> T { + with_runtime(self.runtime, move |runtime| { + runtime.batching.set(true); + let val = f(); + runtime.batching.set(false); + runtime.run_your_effects(); + val + }) + .expect( + "tried to run a batched update in a runtime that has been disposed", + ) + } } impl fmt::Debug for ScopeDisposer { diff --git a/leptos_reactive/src/signal.rs b/leptos_reactive/src/signal.rs index a179d6f..ca15ae9 100644 --- a/leptos_reactive/src/signal.rs +++ b/leptos_reactive/src/signal.rs @@ -1912,7 +1912,7 @@ impl NodeId { runtime.mark_dirty(*self); // notify subscribers - if updated.is_some() { + if updated.is_some() && !runtime.batching.get() { Runtime::run_effects(runtime_id); }; updated diff --git a/leptos_reactive/tests/effect.rs b/leptos_reactive/tests/effect.rs index 2f044ce..799bd7a 100644 --- a/leptos_reactive/tests/effect.rs +++ b/leptos_reactive/tests/effect.rs @@ -1,7 +1,7 @@ #[cfg(not(feature = "stable"))] use leptos_reactive::{ - create_isomorphic_effect, create_memo, create_runtime, create_scope, - create_signal, + create_isomorphic_effect, create_memo, create_runtime, create_rw_signal, + create_scope, create_signal, SignalSet, }; #[cfg(not(feature = "stable"))] @@ -91,3 +91,45 @@ fn untrack_mutes_effect() { }) .dispose() } + +#[cfg(not(feature = "stable"))] +#[test] +fn batching_actually_batches() { + use std::{cell::Cell, rc::Rc}; + + create_scope(create_runtime(), |cx| { + let first_name = create_rw_signal(cx, "Greg".to_string()); + let last_name = create_rw_signal(cx, "Johnston".to_string()); + + // simulate an arbitrary side effect + let count = Rc::new(Cell::new(0)); + + create_isomorphic_effect(cx, { + let count = count.clone(); + move |_| { + _ = first_name(); + _ = last_name(); + + count.set(count.get() + 1); + } + }); + + // runs once initially + assert_eq!(count.get(), 1); + + // individual updates run effect once each + first_name.set("Alice".to_string()); + assert_eq!(count.get(), 2); + + last_name.set("Smith".to_string()); + assert_eq!(count.get(), 3); + + // batched effect only runs twice + cx.batch(move || { + first_name.set("Bob".to_string()); + last_name.set("Williams".to_string()); + }); + assert_eq!(count.get(), 4); + }) + .dispose() +}