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:
commit
bbf2d69b55
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
|
|
|
@ -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.")
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
{
|
||||
|
|
|
@ -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,
|
||||
{
|
||||
|
|
|
@ -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(());
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue