dioxus_signals/
warnings.rs

1//! Warnings that can be triggered by suspicious usage of signals
2
3use warnings::warning;
4
5/// A warning that is triggered when a copy value is used in a higher scope that it is owned by
6#[warning]
7pub fn copy_value_hoisted<T: 'static, S: generational_box::Storage<T>>(
8    value: &crate::CopyValue<T, S>,
9    caller: &'static std::panic::Location<'static>,
10) {
11    let origin_scope = value.origin_scope;
12    if let Ok(current_scope) = dioxus_core::prelude::current_scope_id() {
13        // If the current scope is a descendant of the origin scope or is the same scope, we don't need to warn
14        if origin_scope == current_scope || current_scope.is_descendant_of(origin_scope) {
15            return;
16        }
17        let create_location = value.value.created_at().unwrap();
18        let broken_example = include_str!("../docs/hoist/error.rs");
19        let fixed_example = include_str!("../docs/hoist/fixed_list.rs");
20
21        // Otherwise, warn that the value is being used in a higher scope and may be dropped
22        tracing::warn!(
23            r#"A Copy Value created in {origin_scope:?} (at {create_location}). It will be dropped when that scope is dropped, but it was used in {current_scope:?} (at {caller}) which is not a descendant of the owning scope.
24This may cause reads or writes to fail because the value is dropped while it still held.
25
26Help:
27Copy values (like CopyValue, Signal, Memo, and Resource) are owned by the scope they are created in. If you use the value in a scope that may be dropped after the origin scope,
28it is very easy to use the value after it has been dropped. To fix this, you can move the value to the parent of all of the scopes that it is used in.
29
30Broken example ❌:
31```rust
32{broken_example}
33```
34
35Fixed example ✅:
36```rust
37{fixed_example}
38```"#
39        );
40    }
41}
42
43// Include the examples from the warning to make sure they compile
44#[test]
45#[allow(unused)]
46fn hoist() {
47    mod broken {
48        use dioxus::prelude::*;
49        include!("../docs/hoist/error.rs");
50    }
51    mod fixed {
52        use dioxus::prelude::*;
53        include!("../docs/hoist/fixed_list.rs");
54    }
55}
56
57/// Check if the write happened during a render. If it did, warn the user that this is generally a bad practice.
58#[warning]
59pub fn signal_write_in_component_body(origin: &'static std::panic::Location<'static>) {
60    // Check if the write happened during a render. If it did, we should warn the user that this is generally a bad practice.
61    if dioxus_core::vdom_is_rendering() {
62        tracing::warn!(
63            "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.",
64            origin
65        );
66    }
67}
68
69/// 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.
70#[warning]
71pub fn signal_read_and_write_in_reactive_scope<
72    T: 'static,
73    S: generational_box::Storage<crate::SignalData<T>>,
74>(
75    origin: &'static std::panic::Location<'static>,
76    signal: crate::Signal<T, S>,
77) {
78    // 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.
79    if let Some(reactive_context) = dioxus_core::prelude::ReactiveContext::current() {
80        if let Ok(inner) = crate::Readable::try_read(&signal.inner) {
81            if let Ok(subscribers) = inner.subscribers.lock() {
82                for subscriber in subscribers.iter() {
83                    if reactive_context == *subscriber {
84                        tracing::warn!(
85                            "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}",
86                        );
87                    }
88                }
89            }
90        }
91    }
92}
93
94#[allow(unused)]
95const SIGNAL_READ_WRITE_SAME_SCOPE_HELP: &str = r#"This issue is caused by reading and writing to the same signal in a reactive scope. Components, effects, memos, and resources each have their own a reactive scopes. Reactive scopes rerun when any signal you read inside of them are changed. If you read and write to the same signal in the same scope, the write will cause the scope to rerun and trigger the write again. This can cause an infinite loop.
96
97You can fix the issue by either:
981) Splitting up your state and Writing, reading to different signals:
99
100For example, you could change this broken code:
101
102#[derive(Clone, Copy)]
103struct Counts {
104    count1: i32,
105    count2: i32,
106}
107
108fn app() -> Element {
109    let mut counts = use_signal(|| Counts { count1: 0, count2: 0 });
110
111    use_effect(move || {
112        // This effect both reads and writes to counts
113        counts.write().count1 = counts().count2;
114    })
115}
116
117Into this working code:
118
119fn app() -> Element {
120    let mut count1 = use_signal(|| 0);
121    let mut count2 = use_signal(|| 0);
122
123    use_effect(move || {
124        count1.set(count2());
125    });
126}
1272) Reading and Writing to the same signal in different scopes:
128
129For example, you could change this broken code:
130
131fn app() -> Element {
132    let mut count = use_signal(|| 0);
133
134    use_effect(move || {
135        // This effect both reads and writes to count
136        println!("{}", count());
137        count.set(1);
138    });
139}
140
141
142To this working code:
143
144fn app() -> Element {
145    let mut count = use_signal(|| 0);
146
147    use_effect(move || {
148        count.set(1);
149    });
150    use_effect(move || {
151        println!("{}", count());
152    });
153}
154"#;