mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-10 06:34:20 +00:00
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:
parent
f8cb07e673
commit
a6c025c8ef
10 changed files with 132 additions and 63 deletions
22
Cargo.lock
generated
22
Cargo.lock
generated
|
@ -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"
|
||||||
|
|
|
@ -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"] }
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue