Merge pull request #90 from gbj/self-triggering-effect

Allow triggering an effect to re-run from within the effect
This commit is contained in:
Greg Johnston 2022-11-18 11:28:08 -05:00 committed by GitHub
commit bbf2d69b55
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 63 additions and 75 deletions

View File

@ -4,35 +4,6 @@ This document is intended as a running list of common issues, with example code
## Reactivity
### Don't get and set a signal within the same effect
**Issue**: Sometimes you need access to a signal's current value when setting a new value.
```rust,should_panic
let (a, set_a) = create_signal(cx, false);
create_effect(cx, move |_| {
if !a() {
set_a(true); // ❌ panics: already borrowed
}
});
```
**Solution**: Use the `.update()` function instead.
```rust
let (a, set_a) = create_signal(cx, false);
create_effect(cx, move |_| {
// ✅ updates the signal, which provides you with the current value
set_a.update(|a: &mut bool| {
if *a {
*a = false;
}
})
});
```
### Avoid writing to a signal from an effect
**Issue**: Sometimes you want to update a reactive signal in a way that depends on another signal.

View File

@ -82,7 +82,7 @@ cfg_if! {
.service(css)
.service(
web::scope("/pkg")
.service(Files::new("", "./dist"))
.service(Files::new("", "./pkg"))
.wrap(middleware::Compress::default()),
)
.service(render_app)

View File

@ -6,11 +6,27 @@ pub fn main() {
mount_to_body(|cx| view! { cx, <Tests/> })
}
#[component]
fn SelfUpdatingEffect(cx: Scope) -> Element {
let (a, set_a) = create_signal(cx, false);
create_effect(cx, move |_| {
if !a() {
set_a(true);
}
});
view! { cx,
<h1>"Hello " {move || a().to_string()}</h1>
}
}
#[component]
fn Tests(cx: Scope) -> Element {
view! {
cx,
<div>
<div><SelfUpdatingEffect/></div>
<div><BlockOrders/></div>
//<div><TemplateConsumer/></div>
</div>
@ -98,8 +114,8 @@ fn TemplateConsumer(cx: Scope) -> Element {
view! {
cx,
<div id="template">
<h1>"Template Consumer"</h1>
{cloned_tpl}
/* <h1>"Template Consumer"</h1>
{cloned_tpl} */
</div>
}
}

View File

@ -1,5 +1,5 @@
use leptos_reactive::{create_memo, queue_microtask, Memo, Scope, ScopeDisposer};
use std::{collections::HashMap, fmt::Debug, hash::Hash, ops::IndexMut};
use std::{cell::RefCell, collections::HashMap, fmt::Debug, hash::Hash, ops::IndexMut};
/// Function that maps a `Vec` to another `Vec` via a map function. The mapped `Vec` is lazy
/// computed; its value will only be updated when requested. Modifications to the
@ -28,12 +28,15 @@ where
U: PartialEq + Debug + Clone + 'static,
{
// Previous state used for diffing.
let mut disposers: Vec<Option<ScopeDisposer>> = Vec::new();
let mut prev_items: Option<Vec<T>> = None;
let mut mapped: Vec<U> = Vec::new();
let disposers: RefCell<Vec<Option<ScopeDisposer>>> = RefCell::new(Vec::new());
let prev_items: RefCell<Option<Vec<T>>> = RefCell::new(None);
let mapped: RefCell<Vec<U>> = RefCell::new(Vec::new());
// Diff and update signal each time list is updated.
create_memo(cx, move |_| {
let mut prev_items = prev_items.borrow_mut();
let mut mapped = mapped.borrow_mut();
//let mut mapped = mapped.cloned().unwrap_or_default();
let items = prev_items.take().unwrap_or_default();
let new_items = list();
@ -41,7 +44,7 @@ where
if new_items.is_empty() {
// Fast path for removing all items.
let disposers = std::mem::take(&mut disposers);
let disposers = disposers.take();
// delay disposal until after the current microtask
queue_microtask(move || {
for disposer in disposers.into_iter().flatten() {
@ -50,6 +53,8 @@ where
});
mapped.clear();
} else if items.is_empty() {
let mut disposers = disposers.borrow_mut();
// Fast path for creating items when the existing list is empty.
for new_item in new_items.iter() {
let mut value: Option<U> = None;
@ -60,6 +65,7 @@ where
disposers.push(Some(new_disposer));
}
} else {
let mut disposers = disposers.borrow_mut();
let mut temp = vec![None; new_items.len()];
let mut temp_disposers: Vec<Option<ScopeDisposer>> =
(0..new_items.len()).map(|_| None).collect();
@ -144,10 +150,10 @@ where
}
// 3) In case the new set is shorter than the old, set the length of the mapped array.
mapped.truncate(new_items_len);
disposers.truncate(new_items_len);
disposers.borrow_mut().truncate(new_items_len);
// 4) Return the mapped and new items, for use in next iteration
prev_items = Some(new_items);
*prev_items = Some(new_items);
mapped.to_vec()
})

View File

@ -1,5 +1,6 @@
use crate::{debug_warn, Runtime, Scope, ScopeProperty};
use cfg_if::cfg_if;
use std::cell::RefCell;
use std::fmt::Debug;
/// Effects run a certain chunk of code whenever the signals they depend on change.
@ -45,7 +46,7 @@ use std::fmt::Debug;
/// # }
/// # }).dispose();
/// ```
pub fn create_effect<T>(cx: Scope, f: impl FnMut(Option<T>) -> T + 'static)
pub fn create_effect<T>(cx: Scope, f: impl Fn(Option<T>) -> T + 'static)
where
T: Debug + 'static,
{
@ -84,7 +85,7 @@ where
/// });
/// # assert_eq!(b(), 2);
/// # }).dispose();
pub fn create_isomorphic_effect<T>(cx: Scope, f: impl FnMut(Option<T>) -> T + 'static)
pub fn create_isomorphic_effect<T>(cx: Scope, f: impl Fn(Option<T>) -> T + 'static)
where
T: Debug + 'static,
{
@ -93,7 +94,7 @@ where
}
#[doc(hidden)]
pub fn create_render_effect<T>(cx: Scope, f: impl FnMut(Option<T>) -> T + 'static)
pub fn create_render_effect<T>(cx: Scope, f: impl Fn(Option<T>) -> T + 'static)
where
T: Debug + 'static,
{
@ -108,22 +109,22 @@ slotmap::new_key_type! {
pub(crate) struct Effect<T, F>
where
T: 'static,
F: FnMut(Option<T>) -> T,
F: Fn(Option<T>) -> T,
{
pub(crate) f: F,
pub(crate) value: Option<T>,
pub(crate) value: RefCell<Option<T>>,
}
pub(crate) trait AnyEffect {
fn run(&mut self, id: EffectId, runtime: &Runtime);
fn run(&self, id: EffectId, runtime: &Runtime);
}
impl<T, F> AnyEffect for Effect<T, F>
where
T: 'static,
F: FnMut(Option<T>) -> T,
F: Fn(Option<T>) -> T,
{
fn run(&mut self, id: EffectId, runtime: &Runtime) {
fn run(&self, id: EffectId, runtime: &Runtime) {
// clear previous dependencies
id.cleanup(runtime);
@ -134,7 +135,7 @@ where
// run the effect
let value = self.value.take();
let new_value = (self.f)(value);
self.value = Some(new_value);
*self.value.borrow_mut() = Some(new_value);
// restore the previous observer
runtime.observer.set(prev_observer);
@ -148,7 +149,7 @@ impl EffectId {
effects.get(*self).cloned()
};
if let Some(effect) = effect {
effect.borrow_mut().run(*self, runtime);
effect.run(*self, runtime);
} else {
debug_warn!("[Effect] Trying to run an Effect that has been disposed. This is probably either a logic error in a component that creates and disposes of scopes, or a Resource resolving after its scope has been dropped without having been cleaned up.")
}

View File

@ -54,7 +54,7 @@ use std::fmt::Debug;
/// });
/// # }).dispose();
/// ```
pub fn create_memo<T>(cx: Scope, f: impl FnMut(Option<&T>) -> T + 'static) -> Memo<T>
pub fn create_memo<T>(cx: Scope, f: impl Fn(Option<&T>) -> T + 'static) -> Memo<T>
where
T: PartialEq + Debug + 'static,
{

View File

@ -31,7 +31,7 @@ pub(crate) struct Runtime {
pub scope_cleanups: RefCell<SparseSecondaryMap<ScopeId, Vec<Box<dyn FnOnce()>>>>,
pub signals: RefCell<SlotMap<SignalId, Rc<RefCell<dyn Any>>>>,
pub signal_subscribers: RefCell<SecondaryMap<SignalId, RefCell<HashSet<EffectId>>>>,
pub effects: RefCell<SlotMap<EffectId, Rc<RefCell<dyn AnyEffect>>>>,
pub effects: RefCell<SlotMap<EffectId, Rc<dyn AnyEffect>>>,
pub effect_sources: RefCell<SecondaryMap<EffectId, RefCell<HashSet<SignalId>>>>,
pub resources: RefCell<SlotMap<ResourceId, AnyResource>>,
}
@ -115,27 +115,20 @@ impl Runtime {
}
}
pub(crate) fn create_effect<T>(
&'static self,
f: impl FnMut(Option<T>) -> T + 'static,
) -> EffectId
pub(crate) fn create_effect<T>(&'static self, f: impl Fn(Option<T>) -> T + 'static) -> EffectId
where
T: Any + 'static,
{
let effect = Effect { f, value: None };
let id = {
self.effects
.borrow_mut()
.insert(Rc::new(RefCell::new(effect)))
let effect = Effect {
f,
value: RefCell::new(None),
};
let id = { self.effects.borrow_mut().insert(Rc::new(effect)) };
id.run::<T>(self);
id
}
pub(crate) fn create_memo<T>(
&'static self,
mut f: impl FnMut(Option<&T>) -> T + 'static,
) -> Memo<T>
pub(crate) fn create_memo<T>(&'static self, f: impl Fn(Option<&T>) -> T + 'static) -> Memo<T>
where
T: PartialEq + Any + 'static,
{

View File

@ -415,12 +415,12 @@ impl Scope {
use futures::StreamExt;
if let Some(ref mut shared_context) = *self.runtime.shared_context.borrow_mut() {
let (mut tx, mut rx) = futures::channel::mpsc::channel::<()>(1);
let (tx, mut rx) = futures::channel::mpsc::unbounded();
create_isomorphic_effect(*self, move |_| {
let pending = context.pending_resources.try_with(|n| *n).unwrap_or(0);
if pending == 0 {
_ = tx.try_send(());
_ = tx.unbounded_send(());
}
});

View File

@ -730,7 +730,7 @@ impl SignalId {
effects.get(sub).cloned()
};
if let Some(effect) = effect {
effect.borrow_mut().run(sub, runtime);
effect.run(sub, runtime);
}
}
}

View File

@ -76,7 +76,7 @@ where
{
let location = use_location(cx);
let href = use_resolved_path(cx, move || props.href.to_href()());
let is_active = create_memo(cx, move |_| match href() {
let is_active = create_memo(cx, move |_| match href.get() {
None => false,
Some(to) => {
@ -104,10 +104,10 @@ where
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
view! { cx,
<a
href=move || href().unwrap_or_default()
href=move || href.get().unwrap_or_default()
prop:state={props.state.map(|s| s.to_js_value())}
prop:replace={props.replace}
aria-current=move || if is_active() { Some("page") } else { None }
aria-current=move || if is_active.get() { Some("page") } else { None }
>
{child}
</a>

View File

@ -40,7 +40,7 @@ pub fn Routes(cx: Scope, props: RoutesProps) -> impl IntoChild {
});
// Rebuild the list of nested routes conservatively, and show the root route here
let mut disposers = Vec::<ScopeDisposer>::new();
let disposers = RefCell::new(Vec::<ScopeDisposer>::new());
// iterate over the new matches, reusing old routes when they are the same
// and replacing them with new routes when they differ
@ -54,7 +54,7 @@ pub fn Routes(cx: Scope, props: RoutesProps) -> impl IntoChild {
root_equal.set(true);
next.borrow_mut().clear();
let next_matches = matches();
let next_matches = matches.get();
let prev_matches = prev.as_ref().map(|p| &p.matches);
let prev_routes = prev.as_ref().map(|p| &p.routes);
@ -103,7 +103,7 @@ pub fn Routes(cx: Scope, props: RoutesProps) -> impl IntoChild {
}
},
move || {
matches().get(i).cloned()
matches.with(|m| m.get(i).cloned())
}
);
@ -117,11 +117,12 @@ pub fn Routes(cx: Scope, props: RoutesProps) -> impl IntoChild {
}
});
if disposers.len() > i + 1 {
if disposers.borrow().len() > i + 1 {
let mut disposers = disposers.borrow_mut();
let old_route_disposer = std::mem::replace(&mut disposers[i], disposer);
old_route_disposer.dispose();
} else {
disposers.push(disposer);
disposers.borrow_mut().push(disposer);
}
}
}