diff --git a/leptos_reactive/src/lib.rs b/leptos_reactive/src/lib.rs index 6cd3850db..a2f371804 100644 --- a/leptos_reactive/src/lib.rs +++ b/leptos_reactive/src/lib.rs @@ -125,7 +125,7 @@ use runtime::*; pub use runtime::{ as_child_of_current_owner, batch, create_runtime, current_runtime, on_cleanup, run_as_child, set_current_runtime, - spawn_local_with_current_owner, spawn_local_with_owner, + spawn_local_with_current_owner, spawn_local_with_owner, try_batch, try_spawn_local_with_current_owner, try_spawn_local_with_owner, try_with_owner, untrack, untrack_with_diagnostics, with_current_owner, with_owner, Owner, RuntimeId, ScopedFuture, @@ -143,7 +143,8 @@ pub use suspense::{GlobalSuspenseContext, SuspenseContext}; pub use trigger::*; pub use watch::*; -pub(crate) fn console_warn(s: &str) { +#[doc(hidden)] +pub fn console_warn(s: &str) { cfg_if::cfg_if! { if #[cfg(all(target_arch = "wasm32", any(feature = "csr", feature = "hydrate")))] { web_sys::console::warn_1(&wasm_bindgen::JsValue::from_str(s)); diff --git a/leptos_reactive/src/runtime.rs b/leptos_reactive/src/runtime.rs index 9a91d6d13..61deaf8c9 100644 --- a/leptos_reactive/src/runtime.rs +++ b/leptos_reactive/src/runtime.rs @@ -1397,12 +1397,30 @@ impl Drop for SetObserverOnDrop { /// /// # Panics /// Panics if the runtime has already been disposed. +/// +/// To avoid panicking under any circumstances, use [`try_batch`]. #[cfg_attr( any(debug_assertions, features = "ssr"), instrument(level = "trace", skip_all,) )] #[inline(always)] pub fn batch(f: impl FnOnce() -> T) -> T { + try_batch(f).expect( + "tried to run a batched update in a runtime that has been disposed", + ) +} + +/// Attempts to batch 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. +/// +/// Unlike [`batch`], this will not panic if the runtime has been disposed. +#[cfg_attr( + any(debug_assertions, features = "ssr"), + instrument(level = "trace", skip_all,) +)] +#[inline(always)] +pub fn try_batch(f: impl FnOnce() -> T) -> Result { with_runtime(move |runtime| { let batching = SetBatchingOnDrop(runtime.batching.get()); runtime.batching.set(true); @@ -1415,7 +1433,6 @@ pub fn batch(f: impl FnOnce() -> T) -> T { runtime.run_effects(); val }) - .expect("tried to run a batched update in a runtime that has been disposed") } struct SetBatchingOnDrop(bool); diff --git a/leptos_server/src/action.rs b/leptos_server/src/action.rs index b5ca4e5d1..2f1c7fa50 100644 --- a/leptos_server/src/action.rs +++ b/leptos_server/src/action.rs @@ -1,7 +1,8 @@ //use crate::{ServerFn, ServerFnError}; use leptos_reactive::{ - batch, create_rw_signal, is_suppressing_resource_load, signal_prelude::*, - spawn_local, store_value, use_context, ReadSignal, RwSignal, StoredValue, + console_warn, create_rw_signal, is_suppressing_resource_load, + signal_prelude::*, spawn_local, store_value, try_batch, use_context, + ReadSignal, RwSignal, StoredValue, }; use server_fn::{error::ServerFnUrlError, ServerFn, ServerFnError}; use std::{cell::Cell, future::Future, pin::Pin, rc::Rc}; @@ -93,8 +94,18 @@ where any(debug_assertions, feature = "ssr"), tracing::instrument(level = "trace", skip_all,) )] + #[track_caller] pub fn dispatch(&self, input: I) { - self.0.with_value(|a| a.dispatch(input)) + #[cfg(debug_assertions)] + let loc = std::panic::Location::caller(); + + self.0.with_value(|a| { + a.dispatch( + input, + #[cfg(debug_assertions)] + loc, + ) + }) } /// Create an [Action]. @@ -366,7 +377,11 @@ where any(debug_assertions, feature = "ssr"), tracing::instrument(level = "trace", skip_all,) )] - pub fn dispatch(&self, input: I) { + pub fn dispatch( + &self, + input: I, + #[cfg(debug_assertions)] loc: &'static std::panic::Location<'static>, + ) { if !is_suppressing_resource_load() { let fut = (self.action_fn)(&input); self.input.set(Some(input)); @@ -379,7 +394,7 @@ where pending_dispatches.set(pending_dispatches.get().saturating_sub(1)); spawn_local(async move { let new_value = fut.await; - batch(move || { + let res = try_batch(move || { value.set(Some(new_value)); input.set(None); version.update(|n| *n += 1); @@ -389,6 +404,18 @@ where pending.set(false); } }); + + if res.is_err() { + #[cfg(debug_assertions)] + console_warn(&format!( + "At {loc}, you are dispatching an action in a runtime \ + that has already been disposed. This may be because \ + you are calling `.dispatch()` in the body of a \ + component, during initial server-side rendering. If \ + that's the case, you should probably be using \ + `create_resource` instead of `create_action`." + )); + } }) } }