Comments (4)
I looked into this a bit and I can write up a longer explanation if you'd really like to know; the short version is that it effects are marked "clean" when they finish running, so marking it dirty while it's still dirty doesn't work as you expect (because it then marks itself clean).
I'm struggling to come up with a reason this would be a good idea, though -- could you share a use case?
from leptos.
Thank you for looking into this!
I looked into this a bit and I can write up a longer explanation if you'd really like to know; the short version is that it effects are marked "clean" when they finish running, so marking it dirty while it's still dirty doesn't work as you expect (because it then marks itself clean).
Aah, that makes sense. And while marking it dirty again, it also somehow clears the subscriptions the effect is supposed to be picking up?
Though it might also mean the reproduction is not quite what I encountered originally then (more details below).
I'm struggling to come up with a reason this would be a good idea, though
I agree that it's best avoided if possible, especially the simple repro above which does not involve async code. But it also feels like a pattern that is easy to stumble into in simple cases, and tricky to make sure you don't stumble into in involved applications where the recursion could be many layers removed.
In practice debugging it is also tricky, because the first update will often be correct, and any run after that will be silently broken.
On a more subjective note, I personally find the behaviours above simplify the mental model in terms of leptos
code behaving as much as possible like 'normal' code, where recursion either works or panics (as in recursive RefCell
borrowing).
could you share a use case?
In my case the app/user would be modifying a complex signal that is widely shared, it needs to remain editable and be present in the UI while the edits finish, and then further while an animation runs. When all of that is done, we process the signal and then reset it.
When we wrote the code we had unscoped tasks (as in, a simple wasm_bindgen_futures::spawn_local
with cancel-on-drop), which was working as there was no actual recursion in the reactive sense happening.
Then we introduced scoping (ScopedFuture
), which triggered the problem (not quite sure why, since it would be executed asynchronously. The owner is still relevant somehow?).
The above is the simplest reproduction I could find, which turned out not to require async code, but maybe there are two separate behaviours?
The actual code would have looked something like:
Desugared
// This is a widely shared signal, bound to the UI in a bunch of places
let something: Signal<Option<_>> = todo!();
// Here we spawn a task to do some work and then clear the signal.
let spawned_task = None; // Let's skip RefCell stuff
create_effect(move |_| {
drop(task.take());
let fut = async {
if let Some(v) = something.get() {
sleep().await;
do_stuff();
something.set(None);
}
};
let fut = ScopedFuture::new_current(fut).expect("an owner should be present");
let fut = async move { fut.await.unwrap() }; // In practice we warn if the task fails.
task = Some(spawn_with_handle(fut));
});
// This is a widely shared signal, bound to the UI in a bunch of places.
let something: Signal<Option<_>> = todo!();
// Here we spawn a task to do something, if re-run the task is reset.
// Uses [ScopedFuture] with the effect as the owner internally.
something.for_each_async(move |v| async {
if let Some(v) = something.get() {
sleep().await;
do_stuff();
something.set(None);
}
});
// This code is not quite right (like the timing), but not in relevant-to-the-issue ways.
Reproduction including async, pardon the hackiness:
Repro with sync, async, and scoped async
(requires adding tokio
as a dev dependency tokio = { version = "1", features = ["rt", "macros"]}
)
#[test]
fn recursive_effect() {
use leptos::{SignalGet, SignalGetUntracked, SignalSet};
let _runtime = create_runtime();
let s = leptos::create_rw_signal(0);
leptos::create_isomorphic_effect(move |_| {
let a = s.get();
println!("{a}");
if a == 0 {
return;
}
s.set(0);
});
s.set(1);
s.set(2);
s.set(3);
assert_eq!(0, s.get_untracked()); // Different from OP, does not pass
// Prints:
// 0
// 1
// <panic>
}
#[tokio::test]
async fn recursive_effect_async_unscoped() {
use std::time::Duration;
use leptos::{SignalGet, SignalGetUntracked, SignalSet};
use tokio::time::sleep;
let _runtime = create_runtime();
let s = leptos::create_rw_signal(0);
leptos::create_isomorphic_effect(move |_| {
let a = s.get();
println!("sync {a}");
tokio::spawn(async move {
println!("async {a}");
if a == 0 {
return;
}
s.set(0);
})
});
sleep(Duration::from_secs(1)).await;
s.set(1);
sleep(Duration::from_secs(1)).await;
s.set(2);
sleep(Duration::from_secs(1)).await;
s.set(3);
sleep(Duration::from_secs(1)).await;
assert_eq!(0, s.get_untracked()); // Passes
// Prints:
// sync 0
// async 0
// sync 1
// async 1
// sync 0
// async 0
// sync 2
// async 2
// sync 0
// async 0
// sync 3
// async 3
// sync 0
// async 0
}
#[tokio::test]
async fn recursive_effect_async_scoped() {
use std::time::Duration;
use leptos::{ScopedFuture, SignalGet, SignalGetUntracked, SignalSet};
use tokio::time::sleep;
let _runtime = create_runtime();
let s = leptos::create_rw_signal(0);
leptos::create_isomorphic_effect(move |_| {
let a = s.get();
println!("sync {a}");
let fut = async move {
println!("async {a}");
if a == 0 {
return;
}
s.set(0);
};
let fut = ScopedFuture::new_current(fut).unwrap();
let fut = async move { fut.await.unwrap() };
tokio::spawn(fut);
});
sleep(Duration::from_secs(1)).await;
s.set(1);
sleep(Duration::from_secs(1)).await;
s.set(2);
sleep(Duration::from_secs(1)).await;
s.set(3);
sleep(Duration::from_secs(1)).await;
assert_eq!(0, s.get_untracked()); // Does not pass
// Prints:
// sync 0
// async 0
// sync 1
// async 1
// <panic>
}
Whatever way leptos
decides to go forward with, I would be happy to PR any required change and/or document recursion behaviour.
I guess with the new reactive system coming up it might also make sense to shelve this temporarily, which would be totally fine.
from leptos.
You can somewhat workaround this issue by setting the signal from within a call to queue_microtask
from leptos.
You can somewhat workaround this issue by setting the signal from within a call to
queue_microtask
That indeed works and should be ~equivalent to using wasm_bindgen_futures::spawn_local
to mimic the reproduction cases above. It'll leak the function though, which can cause panics if the signal is disposed of in the meantime.
I would recommend using something like defer_with
to only execute if a parent scope hasn't been cleaned up yet. If you don't use a parent scope then it still won't work. Not sure if it needs to be a parent of the effect, signal, or both. In our case it was both.
from leptos.
Related Issues (20)
- Clippy warnings in some components with rust 1.78 HOT 1
- rkyv, avoid the copy to byte ? HOT 4
- Check ActionForm input names at compile time HOT 4
- Allow server components in islands architecture to call code behind ssr without need for #[server] HOT 1
- porting over some react code to my project, having hydration issues
- leptos_router parent Route attr view is not generating view for dynamic routes HOT 4
- relese build causes SIGSEGV and SIGBUS while dev build is just fine HOT 3
- `HtmlElement::attrs` and `DynAttrs::dyn_attrs` Accept `&'static str` Instead of `Oco<'static, str>` HOT 2
- Notify crate (lib) not compiling when installing cargo-leptos HOT 1
- Query parameters are not properly escaped
- DeserializationError when using create_resource of data that use #[serde(deserialize_with = "xxx")] HOT 3
- Boolean aria attribute values are not handled correctly HOT 3
- LocatorJS integration HOT 1
- Enabling the rkyv feature is failing to compile
- Error during hydration of a recursive component HOT 1
- Introduce "Animated Show" with fallback
- add file upload HOT 1
- `method 'dyn_bindings' is private` when spreading attrs on components
- `try_with` on disposed resource panics instead of returning `None`
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from leptos.