Unify the warning system (#2649)

* unify the warning system

* fix VirtualDom::new warning with a component

* move warnings to dioxuslabs

* also allow writes in the component body when converting from T -> ReadOnlySignal<T>

* fix clippy from merge conflict

---------

Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
Evan Almloff 2024-07-25 00:50:36 +02:00 committed by GitHub
parent f8cb07e673
commit a6c025c8ef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 132 additions and 63 deletions

22
Cargo.lock generated
View file

@ -2526,6 +2526,7 @@ dependencies = [
"tracing", "tracing",
"tracing-fluent-assertions", "tracing-fluent-assertions",
"tracing-subscriber", "tracing-subscriber",
"warnings",
"web-sys", "web-sys",
] ]
@ -2711,6 +2712,7 @@ dependencies = [
"slab", "slab",
"tokio", "tokio",
"tracing", "tracing",
"warnings",
"web-sys", "web-sys",
] ]
@ -2970,6 +2972,7 @@ dependencies = [
"tokio", "tokio",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"warnings",
] ]
[[package]] [[package]]
@ -10363,6 +10366,25 @@ dependencies = [
"try-lock", "try-lock",
] ]
[[package]]
name = "warnings"
version = "0.1.0"
source = "git+https://github.com/DioxusLabs/warnings#9889b96cccb6ac91a8af924cfee51a8895146d08"
dependencies = [
"pin-project",
"warnings-macro",
]
[[package]]
name = "warnings-macro"
version = "0.1.0"
source = "git+https://github.com/DioxusLabs/warnings#9889b96cccb6ac91a8af924cfee51a8895146d08"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.71",
]
[[package]] [[package]]
name = "wasi" name = "wasi"
version = "0.9.0+wasi-snapshot-preview1" version = "0.9.0+wasi-snapshot-preview1"

View file

@ -24,6 +24,7 @@ serde = { version = "1", features = ["derive"], optional = true }
generational-box = { workspace = true } generational-box = { workspace = true }
rustversion = "1.0.17" rustversion = "1.0.17"
const_format = { workspace = true } const_format = { workspace = true }
warnings = { git = "https://github.com/DioxusLabs/warnings" }
[dev-dependencies] [dev-dependencies]
tokio = { workspace = true, features = ["full"] } tokio = { workspace = true, features = ["full"] }

View file

@ -117,12 +117,9 @@ where
P::builder() P::builder()
} }
/// Make sure that this component is currently running as a component, not a function call /// A warning that will trigger if a component is called as a function
#[doc(hidden)] #[warnings::warning]
#[allow(unused)] pub(crate) fn component_called_as_function<C: ComponentFunction<P, M>, P, M>(_: C) {
pub fn verify_component_called_as_component<C: ComponentFunction<P, M>, P, M>(component: C) {
#[cfg(debug_assertions)]
{
// We trim WithOwner from the end of the type name for component with a builder that include a special owner which may not match the function name directly // We trim WithOwner from the end of the type name for component with a builder that include a special owner which may not match the function name directly
let mut type_name = std::any::type_name::<C>(); let mut type_name = std::any::type_name::<C>();
if let Some((_, after_colons)) = type_name.rsplit_once("::") { if let Some((_, after_colons)) = type_name.rsplit_once("::") {
@ -131,8 +128,7 @@ pub fn verify_component_called_as_component<C: ComponentFunction<P, M>, P, M>(co
let component_name = Runtime::with(|rt| { let component_name = Runtime::with(|rt| {
current_scope_id() current_scope_id()
.ok() .ok()
.and_then(|id| rt.get_state(id)) .and_then(|id| rt.get_state(id).map(|scope| scope.name))
.map(|scope| scope.name)
}) })
.ok() .ok()
.flatten(); .flatten();
@ -145,6 +141,12 @@ pub fn verify_component_called_as_component<C: ComponentFunction<P, M>, P, M>(co
// Otherwise the component was called like a function, so we should log an error // Otherwise the component was called like a function, so we should log an error
tracing::error!("It looks like you called the component {type_name} like a function instead of a component. Components should be called with braces like `{type_name} {{ prop: value }}` instead of as a function"); tracing::error!("It looks like you called the component {type_name} like a function instead of a component. Components should be called with braces like `{type_name} {{ prop: value }}` instead of as a function");
} }
/// Make sure that this component is currently running as a component, not a function call
#[doc(hidden)]
#[allow(clippy::no_effect)]
pub fn verify_component_called_as_component<C: ComponentFunction<P, M>, P, M>(component: C) {
component_called_as_function(component);
} }
/// Any component that implements the `ComponentFn` trait can be used as a component. /// Any component that implements the `ComponentFn` trait can be used as a component.

View file

@ -254,8 +254,15 @@ impl VirtualDom {
/// ``` /// ```
/// ///
/// Note: the VirtualDom is not progressed, you must either "run_with_deadline" or use "rebuild" to progress it. /// Note: the VirtualDom is not progressed, you must either "run_with_deadline" or use "rebuild" to progress it.
pub fn new<F: Fn() -> Element + Clone + 'static>(app: F) -> Self { pub fn new(app: fn() -> Element) -> Self {
Self::new_with_props(app, ()) Self::new_with_props(
move || {
use warnings::Warning;
// The root props don't come from a vcomponent so we need to manually rerun them sometimes
crate::properties::component_called_as_function::allow(app)
},
(),
)
} }
/// Create a new VirtualDom with the given properties for the root component. /// Create a new VirtualDom with the given properties for the root component.
@ -313,7 +320,7 @@ impl VirtualDom {
} }
/// Create a new virtualdom and build it immediately /// Create a new virtualdom and build it immediately
pub fn prebuilt<F: Fn() -> Element + Clone + 'static>(app: F) -> Self { pub fn prebuilt(app: fn() -> Element) -> Self {
let mut dom = Self::new(app); let mut dom = Self::new(app);
dom.rebuild_in_place(); dom.rebuild_in_place();
dom dom

View file

@ -22,6 +22,7 @@ slab = { workspace = true }
futures-util = { workspace = true} futures-util = { workspace = true}
generational-box.workspace = true generational-box.workspace = true
rustversion = "1.0.17" rustversion = "1.0.17"
warnings = { git = "https://github.com/DioxusLabs/warnings" }
[dev-dependencies] [dev-dependencies]
futures-util = { workspace = true, default-features = false } futures-util = { workspace = true, default-features = false }

View file

@ -1,3 +1,4 @@
use ::warnings::Warning;
use dioxus_core::prelude::{consume_context, provide_context, spawn, use_hook}; use dioxus_core::prelude::{consume_context, provide_context, spawn, use_hook};
use dioxus_core::Task; use dioxus_core::Task;
use dioxus_signals::*; use dioxus_signals::*;
@ -84,6 +85,8 @@ where
// We do this here so we can capture data with FnOnce // We do this here so we can capture data with FnOnce
// this might not be the best API // this might not be the best API
dioxus_signals::warnings::signal_read_and_write_in_reactive_scope::allow(|| {
dioxus_signals::warnings::signal_write_in_component_body::allow(|| {
if *coroutine.needs_regen.peek() { if *coroutine.needs_regen.peek() {
let (tx, rx) = futures_channel::mpsc::unbounded(); let (tx, rx) = futures_channel::mpsc::unbounded();
let task = spawn(init(rx)); let task = spawn(init(rx));
@ -91,6 +94,8 @@ where
coroutine.task.set(Some(task)); coroutine.task.set(Some(task));
coroutine.needs_regen.set(false); coroutine.needs_regen.set(false);
} }
})
});
coroutine coroutine
} }

View file

@ -110,7 +110,14 @@ pub fn use_reactive<O, D: Dependency>(
non_reactive_data.out() non_reactive_data.out()
}); });
if !first_run && non_reactive_data.changed(&*last_state.peek()) { if !first_run && non_reactive_data.changed(&*last_state.peek()) {
last_state.set(non_reactive_data.out()); use warnings::Warning;
// In use_reactive we do read and write to a signal during rendering to bridge the reactive and non-reactive worlds.
// We ignore
dioxus_signals::warnings::signal_read_and_write_in_reactive_scope::allow(|| {
dioxus_signals::warnings::signal_write_in_component_body::allow(|| {
last_state.set(non_reactive_data.out())
})
});
} }
move || closure(last_state()) move || closure(last_state())
} }

View file

@ -22,6 +22,7 @@ once_cell = "1.18.0"
rustc-hash = { workspace = true } rustc-hash = { workspace = true }
futures-channel = { workspace = true } futures-channel = { workspace = true }
futures-util = { workspace = true } futures-util = { workspace = true }
warnings = { git = "https://github.com/DioxusLabs/warnings" }
[dev-dependencies] [dev-dependencies]
dioxus = { workspace = true } dioxus = { workspace = true }

View file

@ -46,7 +46,13 @@ impl<T: 'static, S: Storage<SignalData<T>>> ReadOnlySignal<T, S> {
/// This should only be used by the `rsx!` macro. /// This should only be used by the `rsx!` macro.
pub fn __set(&mut self, value: T) { pub fn __set(&mut self, value: T) {
use crate::write::Writable; use crate::write::Writable;
use warnings::Warning;
// This is only called when converting T -> ReadOnlySignal<T> which will not cause loops
crate::warnings::signal_write_in_component_body::allow(|| {
crate::warnings::signal_read_and_write_in_reactive_scope::allow(|| {
self.inner.set(value); self.inner.set(value);
});
});
} }
#[doc(hidden)] #[doc(hidden)]

View file

@ -603,30 +603,36 @@ struct SignalSubscriberDrop<T: 'static, S: Storage<SignalData<T>>> {
origin: &'static std::panic::Location<'static>, origin: &'static std::panic::Location<'static>,
} }
impl<T: 'static, S: Storage<SignalData<T>>> Drop for SignalSubscriberDrop<T, S> { pub mod warnings {
fn drop(&mut self) { //! Warnings that can be triggered by suspicious usage of signals
#[cfg(debug_assertions)]
{
tracing::trace!(
"Write on signal at {} finished, updating subscribers",
self.origin
);
use super::*;
use ::warnings::warning;
/// Check if the write happened during a render. If it did, warn the user that this is generally a bad practice.
#[warning]
pub fn signal_write_in_component_body(origin: &'static std::panic::Location<'static>) {
// Check if the write happened during a render. If it did, we should warn the user that this is generally a bad practice. // Check if the write happened during a render. If it did, we should warn the user that this is generally a bad practice.
if dioxus_core::vdom_is_rendering() { if dioxus_core::vdom_is_rendering() {
tracing::warn!( tracing::warn!(
"Write on signal at {} happened while a component was running. Writing to signals during a render can cause infinite rerenders when you read the same signal in the component. Consider writing to the signal in an effect, future, or event handler if possible.", "Write on signal at {} happened while a component was running. Writing to signals during a render can cause infinite rerenders when you read the same signal in the component. Consider writing to the signal in an effect, future, or event handler if possible.",
self.origin origin
); );
} }
}
/// Check if the write happened during a scope that the signal is also subscribed to. If it did, trigger a warning because it will likely cause an infinite loop.
#[warning]
pub fn signal_read_and_write_in_reactive_scope<T: 'static, S: Storage<SignalData<T>>>(
origin: &'static std::panic::Location<'static>,
signal: Signal<T, S>,
) {
// Check if the write happened during a scope that the signal is also subscribed to. If it did, this will probably cause an infinite loop. // Check if the write happened during a scope that the signal is also subscribed to. If it did, this will probably cause an infinite loop.
if let Some(reactive_context) = ReactiveContext::current() { if let Some(reactive_context) = ReactiveContext::current() {
if let Ok(inner) = self.signal.inner.try_read() { if let Ok(inner) = signal.inner.try_read() {
if let Ok(subscribers) = inner.subscribers.lock() { if let Ok(subscribers) = inner.subscribers.lock() {
for subscriber in subscribers.iter() { for subscriber in subscribers.iter() {
if reactive_context == *subscriber { if reactive_context == *subscriber {
let origin = self.origin;
tracing::warn!( tracing::warn!(
"Write on signal at {origin} finished in {reactive_context} which is also subscribed to the signal. This will likely cause an infinite loop. When the write finishes, {reactive_context} will rerun which may cause the write to be rerun again.\nHINT:\n{SIGNAL_READ_WRITE_SAME_SCOPE_HELP}", "Write on signal at {origin} finished in {reactive_context} which is also subscribed to the signal. This will likely cause an infinite loop. When the write finishes, {reactive_context} will rerun which may cause the write to be rerun again.\nHINT:\n{SIGNAL_READ_WRITE_SAME_SCOPE_HELP}",
); );
@ -636,6 +642,17 @@ impl<T: 'static, S: Storage<SignalData<T>>> Drop for SignalSubscriberDrop<T, S>
} }
} }
} }
}
#[allow(clippy::no_effect)]
impl<T: 'static, S: Storage<SignalData<T>>> Drop for SignalSubscriberDrop<T, S> {
fn drop(&mut self) {
tracing::trace!(
"Write on signal at {} finished, updating subscribers",
self.origin
);
warnings::signal_write_in_component_body(self.origin);
warnings::signal_read_and_write_in_reactive_scope::<T, S>(self.origin, self.signal);
self.signal.update_subscribers(); self.signal.update_subscribers();
} }
} }