mirror of
https://github.com/leptos-rs/leptos
synced 2024-11-10 06:44:17 +00:00
docs: runtime warning if you use .track()
outside a tracking context
This commit is contained in:
parent
5bc97654dc
commit
dcec7af4f3
11 changed files with 134 additions and 30 deletions
|
@ -22,6 +22,9 @@ thiserror = "1"
|
|||
tracing = { version = "0.1", optional = true }
|
||||
guardian = "1"
|
||||
|
||||
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
|
||||
web-sys = "0.3"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "time", "macros"] }
|
||||
any_spawner = { workspace = true, features = ["tokio"] }
|
||||
|
|
|
@ -132,6 +132,8 @@ impl<T: Send + Sync + 'static> ToAnySource for ArcMemo<T> {
|
|||
AnySource(
|
||||
Arc::as_ptr(&self.inner) as usize,
|
||||
Arc::downgrade(&self.inner) as Weak<dyn Source + Send + Sync>,
|
||||
#[cfg(debug_assertions)]
|
||||
self.defined_at,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -233,6 +233,8 @@ impl<T: 'static> ToAnySource for ArcAsyncDerived<T> {
|
|||
AnySource(
|
||||
Arc::as_ptr(&self.inner) as usize,
|
||||
Arc::downgrade(&self.inner) as Weak<dyn Source + Send + Sync>,
|
||||
#[cfg(debug_assertions)]
|
||||
self.defined_at,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -93,6 +93,7 @@ impl<T: Send + Sync + 'static> DefinedAt for Memo<T> {
|
|||
}
|
||||
|
||||
impl<T: Send + Sync + 'static> Track for Memo<T> {
|
||||
#[track_caller]
|
||||
fn track(&self) {
|
||||
if let Some(inner) = self.inner.get() {
|
||||
inner.track();
|
||||
|
|
|
@ -33,9 +33,6 @@ thread_local! {
|
|||
}
|
||||
|
||||
impl SpecialNonReactiveZone {
|
||||
// TODO: the fact that this is unused probably means we haven't set diagnostics up at all
|
||||
// we should do that! (i.e., warn if you're doing a reactive access with no owner but you're not
|
||||
// inside a special zone)
|
||||
#[inline(always)]
|
||||
pub(crate) fn is_inside() -> bool {
|
||||
if cfg!(debug_assertions) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use super::{node::ReactiveNode, AnySubscriber};
|
||||
use crate::traits::DefinedAt;
|
||||
use core::{fmt::Debug, hash::Hash};
|
||||
use std::sync::Weak;
|
||||
use std::{panic::Location, sync::Weak};
|
||||
|
||||
pub trait ToAnySource {
|
||||
/// Converts this type to its type-erased equivalent.
|
||||
|
@ -20,7 +21,24 @@ pub trait Source: ReactiveNode {
|
|||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AnySource(pub usize, pub Weak<dyn Source + Send + Sync>);
|
||||
pub struct AnySource(
|
||||
pub(crate) usize,
|
||||
pub(crate) Weak<dyn Source + Send + Sync>,
|
||||
#[cfg(debug_assertions)] pub(crate) &'static Location<'static>,
|
||||
);
|
||||
|
||||
impl DefinedAt for AnySource {
|
||||
fn defined_at(&self) -> Option<&'static Location<'static>> {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
Some(self.2)
|
||||
}
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Debug for AnySource {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
|
|
|
@ -3,39 +3,50 @@ use core::{fmt::Debug, hash::Hash};
|
|||
use std::{cell::RefCell, mem, sync::Weak};
|
||||
|
||||
thread_local! {
|
||||
// TODO this can be a Cell
|
||||
static OBSERVER: RefCell<Option<AnySubscriber>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
pub struct Observer;
|
||||
|
||||
struct SetObserverOnDrop(Option<AnySubscriber>);
|
||||
|
||||
impl Drop for SetObserverOnDrop {
|
||||
fn drop(&mut self) {
|
||||
Observer::set(self.0.take());
|
||||
}
|
||||
}
|
||||
|
||||
impl Observer {
|
||||
pub fn get() -> Option<AnySubscriber> {
|
||||
OBSERVER.with(|o| o.borrow().clone())
|
||||
OBSERVER.with_borrow(Clone::clone)
|
||||
}
|
||||
|
||||
pub(crate) fn is(observer: &AnySubscriber) -> bool {
|
||||
OBSERVER.with(|o| o.borrow().as_ref() == Some(observer))
|
||||
OBSERVER.with_borrow(|o| o.as_ref() == Some(observer))
|
||||
}
|
||||
|
||||
fn take() -> Option<AnySubscriber> {
|
||||
OBSERVER.with(|o| o.borrow_mut().take())
|
||||
fn take() -> SetObserverOnDrop {
|
||||
SetObserverOnDrop(OBSERVER.with_borrow_mut(Option::take))
|
||||
}
|
||||
|
||||
fn set(observer: Option<AnySubscriber>) {
|
||||
OBSERVER.with(|o| *o.borrow_mut() = observer);
|
||||
OBSERVER.with_borrow_mut(|o| *o = observer);
|
||||
}
|
||||
|
||||
fn replace(observer: AnySubscriber) -> Option<AnySubscriber> {
|
||||
OBSERVER.with(|o| mem::replace(&mut *o.borrow_mut(), Some(observer)))
|
||||
fn replace(observer: AnySubscriber) -> SetObserverOnDrop {
|
||||
SetObserverOnDrop(
|
||||
OBSERVER
|
||||
.with(|o| mem::replace(&mut *o.borrow_mut(), Some(observer))),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn untrack<T>(fun: impl FnOnce() -> T) -> T {
|
||||
let prev = Observer::take();
|
||||
let value = fun();
|
||||
Observer::set(prev);
|
||||
value
|
||||
#[cfg(debug_assertions)]
|
||||
let _warning_guard = crate::diagnostics::SpecialNonReactiveZone::enter();
|
||||
|
||||
let _prev = Observer::take();
|
||||
fun()
|
||||
}
|
||||
|
||||
/// Converts a [`Subscriber`] to a type-erased [`AnySubscriber`].
|
||||
|
@ -107,10 +118,8 @@ impl ReactiveNode for AnySubscriber {
|
|||
|
||||
impl AnySubscriber {
|
||||
pub fn with_observer<T>(&self, fun: impl FnOnce() -> T) -> T {
|
||||
let prev = Observer::replace(self.clone());
|
||||
let val = fun();
|
||||
Observer::set(prev);
|
||||
val
|
||||
let _prev = Observer::replace(self.clone());
|
||||
fun()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -70,7 +70,7 @@
|
|||
#![cfg_attr(feature = "nightly", feature(fn_traits))]
|
||||
|
||||
use futures::Stream;
|
||||
use std::{future::Future, pin::Pin};
|
||||
use std::{fmt::Arguments, future::Future, pin::Pin};
|
||||
|
||||
pub mod actions;
|
||||
pub(crate) mod channel;
|
||||
|
@ -98,3 +98,25 @@ pub type PinnedStream<T> = Pin<Box<dyn Stream<Item = T> + Send + Sync>>;
|
|||
pub mod prelude {
|
||||
pub use crate::traits::*;
|
||||
}
|
||||
|
||||
fn log_warning(text: Arguments) {
|
||||
#[cfg(feature = "tracing")]
|
||||
{
|
||||
tracing::warn!(text);
|
||||
}
|
||||
#[cfg(all(
|
||||
not(feature = "tracing"),
|
||||
target_arch = "wasm32",
|
||||
target_os = "unknown"
|
||||
))]
|
||||
{
|
||||
web_sys::console::warn_1(&text.to_string().into());
|
||||
}
|
||||
#[cfg(all(
|
||||
not(feature = "tracing"),
|
||||
not(all(target_arch = "wasm32", target_os = "unknown"))
|
||||
))]
|
||||
{
|
||||
eprintln!("{}", text);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,10 +31,7 @@ impl<T: Send + Sync + 'static> Dispose for RwSignal<T> {
|
|||
}
|
||||
|
||||
impl<T: Send + Sync + 'static> RwSignal<T> {
|
||||
#[cfg_attr(
|
||||
feature = "tracing",
|
||||
tracing::instrument(level = "trace", skip_all,)
|
||||
)]
|
||||
#[track_caller]
|
||||
pub fn new(value: T) -> Self {
|
||||
Self {
|
||||
#[cfg(debug_assertions)]
|
||||
|
@ -44,6 +41,7 @@ impl<T: Send + Sync + 'static> RwSignal<T> {
|
|||
}
|
||||
|
||||
#[inline(always)]
|
||||
#[track_caller]
|
||||
pub fn read_only(&self) -> ReadSignal<T> {
|
||||
ReadSignal {
|
||||
#[cfg(debug_assertions)]
|
||||
|
@ -58,6 +56,7 @@ impl<T: Send + Sync + 'static> RwSignal<T> {
|
|||
}
|
||||
|
||||
#[inline(always)]
|
||||
#[track_caller]
|
||||
pub fn write_only(&self) -> WriteSignal<T> {
|
||||
WriteSignal {
|
||||
#[cfg(debug_assertions)]
|
||||
|
@ -71,6 +70,7 @@ impl<T: Send + Sync + 'static> RwSignal<T> {
|
|||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
#[inline(always)]
|
||||
pub fn split(&self) -> (ReadSignal<T>, WriteSignal<T>) {
|
||||
(self.read_only(), self.write_only())
|
||||
|
|
|
@ -99,6 +99,8 @@ where
|
|||
AnySource(
|
||||
Arc::as_ptr(subs) as usize,
|
||||
Arc::downgrade(subs) as Weak<dyn Source + Send + Sync>,
|
||||
#[cfg(debug_assertions)]
|
||||
self.defined_at().expect("no DefinedAt in debug mode"),
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(unwrap_signal!(self))
|
||||
|
|
|
@ -89,12 +89,40 @@ pub trait Track {
|
|||
fn track(&self);
|
||||
}
|
||||
|
||||
impl<T: Source + ToAnySource> Track for T {
|
||||
impl<T: Source + ToAnySource + DefinedAt> Track for T {
|
||||
#[track_caller]
|
||||
fn track(&self) {
|
||||
if let Some(subscriber) = Observer::get() {
|
||||
subscriber.add_source(self.to_any_source());
|
||||
self.add_subscriber(subscriber);
|
||||
} else {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
use crate::diagnostics::SpecialNonReactiveZone;
|
||||
|
||||
//if !SpecialNonReactiveZone::is_inside() {
|
||||
let called_at = Location::caller();
|
||||
let ty = std::any::type_name::<T>();
|
||||
let defined_at = self
|
||||
.defined_at()
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or_else(|| String::from("{unknown}"));
|
||||
crate::log_warning(format_args!(
|
||||
"At {called_at}, you access a {ty} (defined at \
|
||||
{defined_at}) outside a reactive tracking context. This \
|
||||
might mean your app is not responding to changes in \
|
||||
signal values in the way you expect.\n\nHere’s how to \
|
||||
fix it:\n\n1. If this is inside a `view!` macro, make \
|
||||
sure you are passing a function, not a value.\n ❌ NO \
|
||||
<p>{{x.get() * 2}}</p>\n ✅ YES <p>{{move || x.get() * \
|
||||
2}}</p>\n\n2. If it’s in the body of a component, try \
|
||||
wrapping this access in a closure: \n ❌ NO let y = \
|
||||
x.get() * 2\n ✅ YES let y = move || x.get() * 2.\n\n3. \
|
||||
If you’re *trying* to access the value without tracking, \
|
||||
use `.get_untracked()` or `.with_untracked()` instead."
|
||||
));
|
||||
//}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -254,6 +282,7 @@ where
|
|||
{
|
||||
type Value = <T as With>::Value;
|
||||
|
||||
#[track_caller]
|
||||
fn try_get(&self) -> Option<Self::Value> {
|
||||
self.try_with(Self::Value::clone)
|
||||
}
|
||||
|
@ -266,6 +295,7 @@ pub trait Trigger {
|
|||
pub trait UpdateUntracked: DefinedAt {
|
||||
type Value;
|
||||
|
||||
#[track_caller]
|
||||
fn update_untracked<U>(
|
||||
&self,
|
||||
fun: impl FnOnce(&mut Self::Value) -> U,
|
||||
|
@ -286,6 +316,7 @@ where
|
|||
{
|
||||
type Value = <Self as Writeable>::Value;
|
||||
|
||||
#[track_caller]
|
||||
fn try_update_untracked<U>(
|
||||
&self,
|
||||
fun: impl FnOnce(&mut Self::Value) -> U,
|
||||
|
@ -298,10 +329,12 @@ where
|
|||
pub trait Update {
|
||||
type Value;
|
||||
|
||||
#[track_caller]
|
||||
fn update(&self, fun: impl FnOnce(&mut Self::Value)) {
|
||||
self.try_update(fun);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn maybe_update(&self, fun: impl FnOnce(&mut Self::Value) -> bool) {
|
||||
self.try_maybe_update(|val| {
|
||||
let did_update = fun(val);
|
||||
|
@ -309,6 +342,7 @@ pub trait Update {
|
|||
});
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn try_update<U>(
|
||||
&self,
|
||||
fun: impl FnOnce(&mut Self::Value) -> U,
|
||||
|
@ -328,6 +362,7 @@ where
|
|||
{
|
||||
type Value = <Self as UpdateUntracked>::Value;
|
||||
|
||||
#[track_caller]
|
||||
fn try_maybe_update<U>(
|
||||
&self,
|
||||
fun: impl FnOnce(&mut Self::Value) -> (bool, U),
|
||||
|
@ -340,11 +375,26 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
pub trait Set: Update + IsDisposed {
|
||||
pub trait Set {
|
||||
type Value;
|
||||
|
||||
fn set(&self, value: impl Into<Self::Value>);
|
||||
|
||||
fn try_set(&self, value: impl Into<Self::Value>) -> Option<Self::Value>;
|
||||
}
|
||||
|
||||
impl<T> Set for T
|
||||
where
|
||||
T: Update + IsDisposed,
|
||||
{
|
||||
type Value = <Self as Update>::Value;
|
||||
|
||||
#[track_caller]
|
||||
fn set(&self, value: impl Into<Self::Value>) {
|
||||
self.update(|n| *n = value.into());
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn try_set(&self, value: impl Into<Self::Value>) -> Option<Self::Value> {
|
||||
if self.is_disposed() {
|
||||
Some(value.into())
|
||||
|
@ -355,8 +405,6 @@ pub trait Set: Update + IsDisposed {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T> Set for T where T: Update + IsDisposed {}
|
||||
|
||||
pub trait IsDisposed {
|
||||
fn is_disposed(&self) -> bool;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue