Fix effects triggered from async tasks; improve work scheduling docs (#2370)

* outline work scheduling

* move scheduling code from dirty_scope to scheduler

* introduce queue_effect

* Run effects after all tasks are polled if there are no components rerendered

* add a new test for async effects
This commit is contained in:
Evan Almloff authored and GitHub committed 2024-05-04 15:30:13 +01:00
1 parent 05eaca320f
commit 5290b658fd
11 files changed
+305 -53

No files matched your search

+12 -8
View File
@@ -41,25 +41,29 @@ use crate::use_callback;
/// ```
#[track_caller]
pub fn use_effect(callback: impl FnMut() + 'static) -> Effect {
// let mut run_effect = use_hook(|| CopyValue::new(true));
// use_hook_did_run(move |did_run| run_effect.set(did_run));
let callback = use_callback(callback);
let location = std::panic::Location::caller();
use_hook(|| {
// Inside the effect, we track any reads so that we can rerun the effect if a value the effect reads changes
let (rc, mut changed) = ReactiveContext::new_with_origin(location);
// Spawn a task that will run the effect when:
// 1) The component is first run
// 2) The effect is rerun due to an async read at any time
// 3) The effect is rerun in the same tick that the component is rerun: we need to wait for the component to rerun before we can run the effect again
let queue_effect_for_next_render = move || {
queue_effect(move || rc.run_in(&*callback));
};
queue_effect_for_next_render();
spawn(async move {
loop {
// Run the effect
rc.run_in(&*callback);
// Wait for context to change
let _ = changed.next().await;
// Wait for the dom the be finished with sync work
wait_for_next_render().await;
// Run the effect
queue_effect_for_next_render();
}
});
Effect { rc }
+56 -1
View File
@@ -2,6 +2,7 @@
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::time::Duration;
use dioxus::prelude::*;
use dioxus_core::ElementId;
@@ -42,9 +43,63 @@ async fn effects_rerun() {
);
dom.rebuild_in_place();
dom.wait_for_work().await;
tokio::select! {
_ = dom.wait_for_work() => {}
_ = tokio::time::sleep(Duration::from_millis(500)) => panic!("timed out")
};
let current_counter = counter.borrow();
assert_eq!(current_counter.component, 1);
assert_eq!(current_counter.effect, 1);
}
// https://github.com/DioxusLabs/dioxus/issues/2347
// Effects should rerun when the signal changes if there are no changes to the component
#[tokio::test]
async fn effects_rerun_without_rerender() {
#[derive(Default)]
struct RunCounter {
component: usize,
effect: usize,
}
let counter = Rc::new(RefCell::new(RunCounter::default()));
let mut dom = VirtualDom::new_with_props(
|counter: Rc<RefCell<RunCounter>>| {
counter.borrow_mut().component += 1;
println!("component {}", counter.borrow().component);
let mut signal = use_signal(|| 0);
use_effect({
to_owned![counter];
move || {
counter.borrow_mut().effect += 1;
// This will subscribe the effect to the signal
println!("Signal: {}", signal);
}
});
use_future(move || async move {
for i in 0..10 {
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
println!("future {}", i);
signal += 1;
}
});
rsx! {
div {}
}
},
counter.clone(),
);
dom.rebuild_in_place();
tokio::select! {
_ = dom.wait_for_work() => {}
_ = tokio::time::sleep(Duration::from_millis(500)) => {}
};
let current_counter = counter.borrow();
assert_eq!(current_counter.component, 1);
assert_eq!(current_counter.effect, 11);
}