feat: Trigger primitive and reactive-system cleanups (#838)

This commit is contained in:
Nova 2023-04-10 16:47:52 -05:00 committed by GitHub
parent 764192af36
commit 493c805993
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 438 additions and 152 deletions

View file

@ -3,8 +3,8 @@
use crate::{runtime::with_runtime, Scope};
use std::any::{Any, TypeId};
/// Provides a context value of type `T` to the current reactive [Scope](crate::Scope)
/// and all of its descendants. This can be consumed using [use_context](crate::use_context).
/// Provides a context value of type `T` to the current reactive [`Scope`](crate::Scope)
/// and all of its descendants. This can be consumed using [`use_context`](crate::use_context).
///
/// This is useful for passing values down to components or functions lower in a
/// hierarchy without needs to “prop drill” by passing them through each layer as
@ -61,9 +61,9 @@ where
}
/// Extracts a context value of type `T` from the reactive system by traversing
/// it upwards, beginning from the current [Scope](crate::Scope) and iterating
/// it upwards, beginning from the current [`Scope`](crate::Scope) and iterating
/// through its parents, if any. The context value should have been provided elsewhere
/// using [provide_context](crate::provide_context).
/// using [`provide_context`](crate::provide_context).
///
/// This is useful for passing values down to components or functions lower in a
/// hierarchy without needs to “prop drill” by passing them through each layer as

View file

@ -1,5 +1,3 @@
use cfg_if::cfg_if;
// The point of these diagnostics is to give useful error messages when someone
// tries to access a reactive variable outside the reactive scope. They track when
// you create a signal/memo, and where you access it non-reactively.
@ -23,7 +21,7 @@ pub(crate) struct AccessDiagnostics {}
#[doc(hidden)]
pub struct SpecialNonReactiveZone {}
cfg_if! {
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
use std::cell::Cell;
@ -66,7 +64,7 @@ impl SpecialNonReactiveZone {
#[macro_export]
macro_rules! diagnostics {
($this:ident) => {{
cfg_if! {
cfg_if::cfg_if! {
if #[cfg(debug_assertions)] {
AccessDiagnostics {
defined_at: $this.defined_at,

View file

@ -11,14 +11,14 @@ use std::{any::Any, cell::RefCell, marker::PhantomData, rc::Rc};
/// Effects are intended to run *side-effects* of the system, not to synchronize state
/// *within* the system. In other words: don't write to signals within effects.
/// (If you need to define a signal that depends on the value of other signals, use a
/// derived signal or [create_memo](crate::create_memo)).
/// derived signal or [`create_memo`](crate::create_memo)).
///
/// The effect function is called with an argument containing whatever value it returned
/// the last time it ran. On the initial run, this is `None`.
///
/// By default, effects **do not run on the server**. This means you can call browser-specific
/// APIs within the effect function without causing issues. If you need an effect to run on
/// the server, use [create_isomorphic_effect].
/// the server, use [`create_isomorphic_effect`].
/// ```
/// # use leptos_reactive::*;
/// # use log::*;
@ -76,7 +76,7 @@ where
}
}
/// Creates an effect; unlike effects created by [create_effect], isomorphic effects will run on
/// Creates an effect; unlike effects created by [`create_effect`], isomorphic effects will run on
/// the server as well as the client.
/// ```
/// # use leptos_reactive::*;

View file

@ -17,18 +17,19 @@
//! Here are the most commonly-used functions and types you'll need to build a reactive system:
//!
//! ### Signals
//! 1. *Signals:* [create_signal](crate::create_signal), which returns a ([ReadSignal](crate::ReadSignal),
//! [WriteSignal](crate::WriteSignal)) tuple, or [create_rw_signal](crate::create_rw_signal), which returns
//! a signal [RwSignal](crate::RwSignal) without this read-write segregation.
//! 1. *Signals:* [`create_signal`](crate::create_signal), which returns a ([`ReadSignal`](crate::ReadSignal),
//! [`WriteSignal`](crate::WriteSignal)) tuple, or [`create_rw_signal`](crate::create_rw_signal), which returns
//! a signal [`RwSignal`](crate::RwSignal) without this read-write segregation.
//! 2. *Derived Signals:* any function that relies on another signal.
//! 3. *Memos:* [create_memo](crate::create_memo), which returns a [Memo](crate::Memo).
//! 4. *Resources:* [create_resource], which converts an `async` [std::future::Future] into a
//! synchronous [Resource](crate::Resource) signal.
//! 3. *Memos:* [`create_memo`], which returns a [`Memo`](crate::Memo).
//! 4. *Resources:* [`create_resource`], which converts an `async` [`Future`](std::future::Future) into a
//! synchronous [`Resource`](crate::Resource) signal.
//! 5. *Triggers:* [`create_trigger`], creates a purely reactive [`Trigger`] primitive without any associated state.
//!
//! ### Effects
//! 1. Use [create_effect](crate::create_effect) when you need to synchronize the reactive system
//! 1. Use [`create_effect`](crate::create_effect) when you need to synchronize the reactive system
//! with something outside it (for example: logging to the console, writing to a file or local storage)
//! 2. The Leptos DOM renderer wraps any [Fn] in your template with [create_effect](crate::create_effect), so
//! 2. The Leptos DOM renderer wraps any [`Fn`] in your template with [`create_effect`](crate::create_effect), so
//! components you write do *not* need explicit effects to synchronize with the DOM.
//!
//! ### Example
@ -90,6 +91,7 @@ mod spawn;
mod spawn_microtask;
mod stored_value;
pub mod suspense;
mod trigger;
pub use context::*;
pub use diagnostics::SpecialNonReactiveZone;
@ -109,6 +111,7 @@ pub use spawn::*;
pub use spawn_microtask::*;
pub use stored_value::*;
pub use suspense::SuspenseContext;
pub use trigger::*;
mod macros {
macro_rules! debug_warn {

View file

@ -5,7 +5,6 @@ use crate::{
SignalDispose, SignalGet, SignalGetUntracked, SignalStream, SignalWith,
SignalWithUntracked,
};
use cfg_if::cfg_if;
use std::{any::Any, cell::RefCell, fmt::Debug, marker::PhantomData, rc::Rc};
/// Creates an efficient derived reactive value based on other reactive values.
@ -21,7 +20,7 @@ use std::{any::Any, cell::RefCell, fmt::Debug, marker::PhantomData, rc::Rc};
/// create a derived signal. But if the derivation calculation is expensive, you should
/// create a memo.
///
/// As with [create_effect](crate::create_effect), the argument to the memo function is the previous value,
/// As with [`create_effect`](crate::create_effect), the argument to the memo function is the previous value,
/// i.e., the current value of the memo, which will be `None` for the initial calculation.
///
/// ```
@ -99,7 +98,7 @@ where
/// create a derived signal. But if the derivation calculation is expensive, you should
/// create a memo.
///
/// As with [create_effect](crate::create_effect), the argument to the memo function is the previous value,
/// As with [`create_effect`](crate::create_effect), the argument to the memo function is the previous value,
/// i.e., the current value of the memo, which will be `None` for the initial calculation.
///
/// ## Core Trait Implementations

View file

@ -8,13 +8,22 @@ slotmap::new_key_type! {
#[derive(Clone)]
pub(crate) struct ReactiveNode {
pub value: Rc<RefCell<dyn Any>>,
pub value: Option<Rc<RefCell<dyn Any>>>,
pub state: ReactiveNodeState,
pub node_type: ReactiveNodeType,
}
impl ReactiveNode {
pub fn value(&self) -> Rc<RefCell<dyn Any>> {
self.value
.clone()
.expect("ReactiveNode.value to have a value")
}
}
#[derive(Clone)]
pub(crate) enum ReactiveNodeType {
Trigger,
Signal,
Memo { f: Rc<dyn AnyComputation> },
Effect { f: Rc<dyn AnyComputation> },

View file

@ -20,19 +20,19 @@ use std::{
rc::Rc,
};
/// Creates [Resource](crate::Resource), which is a signal that reflects the
/// Creates a [`Resource`](crate::Resource), which is a signal that reflects the
/// current state of an asynchronous task, allowing you to integrate `async`
/// [Future]s into the synchronous reactive system.
/// [`Future`]s into the synchronous reactive system.
///
/// Takes a `fetcher` function that generates a [Future] when called and a
/// Takes a `fetcher` function that generates a [`Future`] when called and a
/// `source` signal that provides the argument for the `fetcher`. Whenever the
/// value of the `source` changes, a new [Future] will be created and run.
/// value of the `source` changes, a new [`Future`] will be created and run.
///
/// When server-side rendering is used, the server will handle running the
/// [Future] and will stream the result to the client. This process requires the
/// output type of the Future to be [Serializable]. If your output cannot be
/// serialized, or you just want to make sure the [Future] runs locally, use
/// [create_local_resource()].
/// [`Future`] and will stream the result to the client. This process requires the
/// output type of the Future to be [`Serializable`]. If your output cannot be
/// serialized, or you just want to make sure the [`Future`] runs locally, use
/// [`create_local_resource()`].
///
/// ```
/// # use leptos_reactive::*;
@ -79,14 +79,14 @@ where
create_resource_with_initial_value(cx, source, fetcher, initial_value)
}
/// Creates a [Resource](crate::Resource) with the given initial value, which
/// will only generate and run a [Future] using the `fetcher` when the `source` changes.
/// Creates a [`Resource`](crate::Resource) with the given initial value, which
/// will only generate and run a [`Future`] using the `fetcher` when the `source` changes.
///
/// When server-side rendering is used, the server will handle running the
/// [Future] and will stream the result to the client. This process requires the
/// output type of the Future to be [Serializable]. If your output cannot be
/// serialized, or you just want to make sure the [Future] runs locally, use
/// [create_local_resource_with_initial_value()].
/// [`Future`] and will stream the result to the client. This process requires the
/// output type of the Future to be [`Serializable`]. If your output cannot be
/// serialized, or you just want to make sure the [`Future`] runs locally, use
/// [`create_local_resource_with_initial_value()`].
#[cfg_attr(
debug_assertions,
instrument(
@ -120,7 +120,7 @@ where
)
}
/// Creates a “blocking” [Resource](crate::Resource). When server-side rendering is used,
/// Creates a “blocking” [`Resource`](crate::Resource). When server-side rendering is used,
/// this resource will cause any `<Suspense/>` you read it under to block the initial
/// chunk of HTML from being sent to the client. This means that if you set things like
/// HTTP headers or `<head>` metadata in that `<Suspense/>`, that header material will
@ -228,16 +228,16 @@ where
}
}
/// Creates a _local_ [Resource](crate::Resource), which is a signal that
/// Creates a _local_ [`Resource`](crate::Resource), which is a signal that
/// reflects the current state of an asynchronous task, allowing you to
/// integrate `async` [Future]s into the synchronous reactive system.
/// integrate `async` [`Future`]s into the synchronous reactive system.
///
/// Takes a `fetcher` function that generates a [Future] when called and a
/// Takes a `fetcher` function that generates a [`Future`] when called and a
/// `source` signal that provides the argument for the `fetcher`. Whenever the
/// value of the `source` changes, a new [Future] will be created and run.
/// value of the `source` changes, a new [`Future`] will be created and run.
///
/// Unlike [create_resource()], this [Future] is always run on the local system
/// and therefore it's result type does not need to be [Serializable].
/// Unlike [`create_resource()`], this [`Future`] is always run on the local system
/// and therefore it's result type does not need to be [`Serializable`].
///
/// ```
/// # use leptos_reactive::*;
@ -273,13 +273,13 @@ where
create_local_resource_with_initial_value(cx, source, fetcher, initial_value)
}
/// Creates a _local_ [Resource](crate::Resource) with the given initial value,
/// which will only generate and run a [Future] using the `fetcher` when the
/// Creates a _local_ [`Resource`](crate::Resource) with the given initial value,
/// which will only generate and run a [`Future`] using the `fetcher` when the
/// `source` changes.
///
/// Unlike [create_resource_with_initial_value()], this [Future] will always run
/// Unlike [`create_resource_with_initial_value()`], this [`Future`] will always run
/// on the local system and therefore its output type does not need to be
/// [Serializable].
/// [`Serializable`].
#[cfg_attr(
debug_assertions,
instrument(
@ -443,7 +443,7 @@ where
/// resource is still pending). Also subscribes the running effect to this
/// resource.
///
/// If you want to get the value without cloning it, use [Resource::with].
/// If you want to get the value without cloning it, use [`Resource::with`].
/// (`value.read(cx)` is equivalent to `value.with(cx, T::clone)`.)
#[track_caller]
pub fn read(&self, cx: Scope) -> Option<T>
@ -463,10 +463,10 @@ where
/// Applies a function to the current value of the resource, and subscribes
/// the running effect to this resource. If the resource hasn't yet
/// resolved, the function won't be called and this will return
/// [Option::None].
/// [`Option::None`].
///
/// If you want to get the value by cloning it, you can use
/// [Resource::read].
/// [`Resource::read`].
#[track_caller]
pub fn with<U>(&self, cx: Scope, f: impl FnOnce(&T) -> U) -> Option<U> {
let location = std::panic::Location::caller();
@ -501,8 +501,8 @@ where
});
}
/// Returns a [std::future::Future] that will resolve when the resource has loaded,
/// yield its [ResourceId] and a JSON string.
/// Returns a [`Future`] that will resolve when the resource has loaded,
/// yield its [`ResourceId`] and a JSON string.
#[cfg(any(feature = "ssr", doc))]
pub async fn to_serialization_resolver(
&self,
@ -526,17 +526,17 @@ where
/// A signal that reflects the
/// current state of an asynchronous task, allowing you to integrate `async`
/// [Future]s into the synchronous reactive system.
/// [`Future`]s into the synchronous reactive system.
///
/// Takes a `fetcher` function that generates a [Future] when called and a
/// Takes a `fetcher` function that generates a [`Future`] when called and a
/// `source` signal that provides the argument for the `fetcher`. Whenever the
/// value of the `source` changes, a new [Future] will be created and run.
/// value of the `source` changes, a new [`Future`] will be created and run.
///
/// When server-side rendering is used, the server will handle running the
/// [Future] and will stream the result to the client. This process requires the
/// output type of the Future to be [Serializable]. If your output cannot be
/// serialized, or you just want to make sure the [Future] runs locally, use
/// [create_local_resource()].
/// [`Future`] and will stream the result to the client. This process requires the
/// output type of the Future to be [`Serializable`]. If your output cannot be
/// serialized, or you just want to make sure the [`Future`] runs locally, use
/// [`create_local_resource()`].
///
/// ```
/// # use leptos_reactive::*;
@ -583,7 +583,7 @@ where
// Resources
slotmap::new_key_type! {
/// Unique ID assigned to a [Resource](crate::Resource).
/// Unique ID assigned to a [`Resource`](crate::Resource).
pub struct ResourceId;
}

View file

@ -4,8 +4,8 @@ use crate::{
node::{NodeId, ReactiveNode, ReactiveNodeState, ReactiveNodeType},
AnyComputation, AnyResource, Effect, Memo, MemoState, ReadSignal,
ResourceId, ResourceState, RwSignal, Scope, ScopeDisposer, ScopeId,
ScopeProperty, SerializableResource, StoredValueId, UnserializableResource,
WriteSignal,
ScopeProperty, SerializableResource, StoredValueId, Trigger,
UnserializableResource, WriteSignal,
};
use cfg_if::cfg_if;
use core::hash::BuildHasherDefault;
@ -117,15 +117,16 @@ impl Runtime {
// memos and effects rerun
// signals simply have their value
let changed = match node.node_type {
ReactiveNodeType::Signal => true,
ReactiveNodeType::Memo { f }
| ReactiveNodeType::Effect { f } => {
ReactiveNodeType::Signal | ReactiveNodeType::Trigger => true,
ReactiveNodeType::Memo { ref f }
| ReactiveNodeType::Effect { ref f } => {
let value = node.value();
// set this node as the observer
self.with_observer(node_id, move || {
// clean up sources of this memo/effect
self.cleanup(node_id);
f.run(Rc::clone(&node.value))
f.run(value)
})
}
};
@ -191,12 +192,17 @@ impl Runtime {
pub(crate) fn mark_dirty(&self, node: NodeId) {
//crate::macros::debug_warn!("marking {node:?} dirty");
let mut nodes = self.nodes.borrow_mut();
let mut pending_effects = self.pending_effects.borrow_mut();
let subscribers = self.node_subscribers.borrow();
let current_observer = self.observer.get();
// mark self dirty
if let Some(current_node) = nodes.get_mut(node) {
if current_node.state == ReactiveNodeState::DirtyMarked {
return;
}
let mut pending_effects = self.pending_effects.borrow_mut();
let subscribers = self.node_subscribers.borrow();
let current_observer = self.observer.get();
// mark self dirty
Runtime::mark(
node,
current_node,
@ -325,13 +331,7 @@ impl Runtime {
}
}
pub(crate) fn run_effects(runtime_id: RuntimeId) {
_ = with_runtime(runtime_id, |runtime| {
runtime.run_your_effects();
});
}
pub(crate) fn run_your_effects(&self) {
pub(crate) fn run_effects(&self) {
if !self.batching.get() {
let effects = self.pending_effects.take();
for effect_id in effects {
@ -384,7 +384,7 @@ pub(crate) fn with_runtime<T>(
#[doc(hidden)]
#[must_use = "Runtime will leak memory if Runtime::dispose() is never called."]
/// Creates a new reactive [Runtime]. This should almost always be handled by the framework.
/// Creates a new reactive [`Runtime`]. This should almost always be handled by the framework.
pub fn create_runtime() -> RuntimeId {
cfg_if! {
if #[cfg(any(feature = "csr", feature = "hydrate"))] {
@ -397,17 +397,17 @@ pub fn create_runtime() -> RuntimeId {
#[cfg(not(any(feature = "csr", feature = "hydrate")))]
slotmap::new_key_type! {
/// Unique ID assigned to a [Runtime](crate::Runtime).
/// Unique ID assigned to a Runtime.
pub struct RuntimeId;
}
/// Unique ID assigned to a [Runtime](crate::Runtime).
/// Unique ID assigned to a Runtime.
#[cfg(any(feature = "csr", feature = "hydrate"))]
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct RuntimeId;
impl RuntimeId {
/// Removes the runtime, disposing all its child [Scope](crate::Scope)s.
/// Removes the runtime, disposing all its child [`Scope`](crate::Scope)s.
pub fn dispose(self) {
cfg_if! {
if #[cfg(not(any(feature = "csr", feature = "hydrate")))] {
@ -468,13 +468,35 @@ impl RuntimeId {
ret
}
#[track_caller]
#[inline(always)] // only because it's placed here to fit in with the other create methods
pub(crate) fn create_trigger(self) -> Trigger {
let id = with_runtime(self, |runtime| {
runtime.nodes.borrow_mut().insert(ReactiveNode {
value: None,
state: ReactiveNodeState::Clean,
node_type: ReactiveNodeType::Trigger,
})
})
.expect(
"tried to create a trigger in a runtime that has been disposed",
);
Trigger {
id,
runtime: self,
#[cfg(debug_assertions)]
defined_at: std::panic::Location::caller(),
}
}
pub(crate) fn create_concrete_signal(
self,
value: Rc<RefCell<dyn Any>>,
) -> NodeId {
with_runtime(self, |runtime| {
runtime.nodes.borrow_mut().insert(ReactiveNode {
value,
value: Some(value),
state: ReactiveNodeState::Clean,
node_type: ReactiveNodeType::Signal,
})
@ -539,7 +561,7 @@ impl RuntimeId {
values
.map(|value| {
signals.insert(ReactiveNode {
value: Rc::new(RefCell::new(value)),
value: Some(Rc::new(RefCell::new(value))),
state: ReactiveNodeState::Clean,
node_type: ReactiveNodeType::Signal,
})
@ -598,7 +620,7 @@ impl RuntimeId {
) -> NodeId {
with_runtime(self, |runtime| {
let id = runtime.nodes.borrow_mut().insert(ReactiveNode {
value: Rc::clone(&value),
value: Some(Rc::clone(&value)),
state: ReactiveNodeState::Clean,
node_type: ReactiveNodeType::Effect {
f: Rc::clone(&effect),
@ -625,7 +647,7 @@ impl RuntimeId {
) -> NodeId {
with_runtime(self, |runtime| {
runtime.nodes.borrow_mut().insert(ReactiveNode {
value,
value: Some(value),
// memos are lazy, so are dirty when created
// will be run the first time we ask for it
state: ReactiveNodeState::Dirty,
@ -775,12 +797,13 @@ impl Runtime {
f
}
/// Do not call on triggers
pub(crate) fn get_value(
&self,
node_id: NodeId,
) -> Option<Rc<RefCell<dyn Any>>> {
let signals = self.nodes.borrow();
signals.get(node_id).map(|node| Rc::clone(&node.value))
signals.get(node_id).map(|node| node.value())
}
}

View file

@ -82,7 +82,7 @@ pub fn run_scope_undisposed<T>(
/// when it is removed from the list.
///
/// Every other function in this crate takes a `Scope` as its first argument. Since `Scope`
/// is [Copy] and `'static` this does not add much overhead or lifetime complexity.
/// is [`Copy`] and `'static` this does not add much overhead or lifetime complexity.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Scope {
#[doc(hidden)]
@ -226,7 +226,7 @@ impl Scope {
///
/// This will
/// 1. dispose of all child `Scope`s
/// 2. run all cleanup functions defined for this scope by [on_cleanup](crate::on_cleanup).
/// 2. run all cleanup functions defined for this scope by [`on_cleanup`](crate::on_cleanup).
/// 3. dispose of all signals, effects, and resources owned by this `Scope`.
pub fn dispose(self) {
_ = with_runtime(self.runtime, |runtime| {
@ -264,7 +264,8 @@ impl Scope {
if let Some(owned) = owned {
for property in owned {
match property {
ScopeProperty::Signal(id) => {
ScopeProperty::Signal(id)
| ScopeProperty::Trigger(id) => {
// remove the signal
runtime.nodes.borrow_mut().remove(id);
let subs = runtime
@ -339,7 +340,7 @@ fn push_cleanup(cx: Scope, cleanup_fn: Box<dyn FnOnce()>) {
});
}
/// Creates a cleanup function, which will be run when a [Scope] is disposed.
/// Creates a cleanup function, which will be run when a [`Scope`] is disposed.
///
/// It runs after child scopes have been disposed, but before signals, effects, and resources
/// are invalidated.
@ -349,34 +350,35 @@ pub fn on_cleanup(cx: Scope, cleanup_fn: impl FnOnce() + 'static) {
}
slotmap::new_key_type! {
/// Unique ID assigned to a [Scope](crate::Scope).
/// Unique ID assigned to a [`Scope`](crate::Scope).
pub struct ScopeId;
}
#[derive(Debug)]
pub(crate) enum ScopeProperty {
Trigger(NodeId),
Signal(NodeId),
Effect(NodeId),
Resource(ResourceId),
StoredValue(StoredValueId),
}
/// Creating a [Scope](crate::Scope) gives you a disposer, which can be called
/// Creating a [`Scope`](crate::Scope) gives you a disposer, which can be called
/// to dispose of that reactive scope.
///
/// This will
/// 1. dispose of all child `Scope`s
/// 2. run all cleanup functions defined for this scope by [on_cleanup](crate::on_cleanup).
/// 2. run all cleanup functions defined for this scope by [`on_cleanup`](crate::on_cleanup).
/// 3. dispose of all signals, effects, and resources owned by this `Scope`.
#[repr(transparent)]
pub struct ScopeDisposer(pub(crate) Scope);
impl ScopeDisposer {
/// Disposes of a reactive [Scope](crate::Scope).
/// Disposes of a reactive [`Scope`](crate::Scope).
///
/// This will
/// 1. dispose of all child `Scope`s
/// 2. run all cleanup functions defined for this scope by [on_cleanup](crate::on_cleanup).
/// 2. run all cleanup functions defined for this scope by [`on_cleanup`](crate::on_cleanup).
/// 3. dispose of all signals, effects, and resources owned by this `Scope`.
#[inline(always)]
pub fn dispose(self) {
@ -385,20 +387,20 @@ impl ScopeDisposer {
}
impl Scope {
/// Returns IDs for all [Resource](crate::Resource)s found on any scope.
/// Returns IDs for all [`Resource`](crate::Resource)s found on any scope.
pub fn all_resources(&self) -> Vec<ResourceId> {
with_runtime(self.runtime, |runtime| runtime.all_resources())
.unwrap_or_default()
}
/// Returns IDs for all [Resource](crate::Resource)s found on any scope that are
/// Returns IDs for all [`Resource`](crate::Resource)s found on any scope that are
/// pending from the server.
pub fn pending_resources(&self) -> Vec<ResourceId> {
with_runtime(self.runtime, |runtime| runtime.pending_resources())
.unwrap_or_default()
}
/// Returns IDs for all [Resource](crate::Resource)s found on any scope.
/// Returns IDs for all [`Resource`](crate::Resource)s found on any scope.
pub fn serialization_resolvers(
&self,
) -> FuturesUnordered<PinnedFuture<(ResourceId, String)>> {
@ -408,7 +410,7 @@ impl Scope {
.unwrap_or_default()
}
/// Registers the given [SuspenseContext](crate::SuspenseContext) with the current scope,
/// Registers the given [`SuspenseContext`](crate::SuspenseContext) with the current scope,
/// calling the `resolver` when its resources are all resolved.
pub fn register_suspense(
&self,
@ -522,7 +524,7 @@ impl Scope {
runtime.batching.set(batching.1);
std::mem::forget(batching);
runtime.run_your_effects();
runtime.run_effects();
val
})
.expect(

View file

@ -9,7 +9,7 @@ use std::{
/// Creates a conditional signal that only notifies subscribers when a change
/// in the source signals value changes whether it is equal to the key value
/// (as determined by [PartialEq].)
/// (as determined by [`PartialEq`].)
///
/// **You probably dont need this,** but it can be a very useful optimization
/// in certain situations (e.g., “set the class `selected` if `selected() == this_row_index`)

View file

@ -4,7 +4,7 @@ use std::rc::Rc;
use thiserror::Error;
/// Describes errors that can occur while serializing and deserializing data,
/// typically during the process of streaming [Resource](crate::Resource)s from
/// typically during the process of streaming [`Resource`](crate::Resource)s from
/// the server to the client.
#[derive(Debug, Clone, Error)]
pub enum SerializationError {
@ -19,7 +19,7 @@ pub enum SerializationError {
/// Describes an object that can be serialized to or from a supported format
/// Currently those are JSON and Cbor
///
/// This is primarily used for serializing and deserializing [Resource](crate::Resource)s
/// This is primarily used for serializing and deserializing [`Resource`](crate::Resource)s
/// so they can begin on the server and be resolved on the client, but can be used
/// for any data that needs to be serialized/deserialized.
///

View file

@ -8,7 +8,6 @@ use crate::{
runtime::{with_runtime, RuntimeId},
Runtime, Scope, ScopeProperty,
};
use cfg_if::cfg_if;
use futures::Stream;
use std::{
any::Any, cell::RefCell, fmt::Debug, marker::PhantomData, pin::Pin, rc::Rc,
@ -110,7 +109,7 @@ pub trait SignalGet<T> {
/// the running effect to this signal.
///
/// # Panics
/// Panics if you try to access a signal that was created in a [Scope] that has been disposed.
/// Panics if you try to access a signal that was created in a [`Scope`] that has been disposed.
#[track_caller]
fn get(&self) -> T;
@ -126,7 +125,7 @@ pub trait SignalWith<T> {
/// the running effect to this signal.
///
/// # Panics
/// Panics if you try to access a signal that was created in a [Scope] that has been disposed.
/// Panics if you try to access a signal that was created in a [`Scope`] that has been disposed.
#[track_caller]
fn with<O>(&self, f: impl FnOnce(&T) -> O) -> O;
@ -198,7 +197,7 @@ pub trait SignalGetUntracked<T> {
/// current scope.
///
/// # Panics
/// Panics if you try to access a signal that was created in a [Scope] that has been disposed.
/// Panics if you try to access a signal that was created in a [`Scope`] that has been disposed.
#[track_caller]
fn get_untracked(&self) -> T;
@ -215,7 +214,7 @@ pub trait SignalWithUntracked<T> {
/// value without creating a dependency on the current scope.
///
/// # Panics
/// Panics if you try to access a signal that was created in a [Scope] that has been disposed.
/// Panics if you try to access a signal that was created in a [`Scope`] that has been disposed.
#[track_caller]
fn with_untracked<O>(&self, f: impl FnOnce(&T) -> O) -> O;
@ -276,7 +275,7 @@ pub trait SignalStream<T> {
/// whenever it changes.
///
/// # Panics
/// Panics if you try to access a signal that was created in a [Scope] that has been disposed.
/// Panics if you try to access a signal that was created in a [`Scope`] that has been disposed.
// We're returning an opaque type until impl trait in trait
// positions are stabilized, and also so any underlying
// changes are non-breaking
@ -284,7 +283,7 @@ pub trait SignalStream<T> {
fn to_stream(&self, cx: Scope) -> Pin<Box<dyn Stream<Item = T>>>;
}
/// This trait allows disposing a signal before its [Scope] has been disposed.
/// This trait allows disposing a signal before its [`Scope`] has been disposed.
pub trait SignalDispose {
/// Disposes of the signal. This:
/// 1. Detaches the signal from the reactive graph, preventing it from triggering
@ -300,8 +299,8 @@ pub trait SignalDispose {
/// and notifies other code when it has changed. This is the
/// core primitive of Leptoss reactive system.
///
/// Takes a reactive [Scope] and the initial value as arguments,
/// and returns a tuple containing a [ReadSignal] and a [WriteSignal],
/// Takes a reactive [`Scope`] and the initial value as arguments,
/// and returns a tuple containing a [`ReadSignal`] and a [`WriteSignal`],
/// each of which can be called as a function.
///
/// ```
@ -353,7 +352,7 @@ pub fn create_signal<T>(
s
}
/// Works exactly as [create_signal], but creates multiple signals at once.
/// Works exactly as [`create_signal`], but creates multiple signals at once.
#[cfg_attr(
debug_assertions,
instrument(
@ -373,7 +372,7 @@ pub fn create_many_signals<T>(
cx.runtime.create_many_signals_with_map(cx, values, |x| x)
}
/// Works exactly as [create_many_signals], but applies the map function to each signal pair.
/// Works exactly as [`create_many_signals`], but applies the map function to each signal pair.
#[cfg_attr(
debug_assertions,
instrument(
@ -398,7 +397,7 @@ where
}
/// Creates a signal that always contains the most recent value emitted by a
/// [Stream](futures::stream::Stream).
/// [`Stream`](futures::stream::Stream).
/// If the stream has not yet emitted a value since the signal was created, the signal's
/// value will be `None`.
///
@ -419,7 +418,7 @@ pub fn create_signal_from_stream<T>(
#[allow(unused_mut)] // allowed because needed for SSR
mut stream: impl Stream<Item = T> + Unpin + 'static,
) -> ReadSignal<Option<T>> {
cfg_if! {
cfg_if::cfg_if! {
if #[cfg(feature = "ssr")] {
_ = stream;
let (read, _) = create_signal(cx, None);
@ -445,7 +444,7 @@ pub fn create_signal_from_stream<T>(
/// and notifies other code when it has changed. This is the
/// core primitive of Leptoss reactive system.
///
/// `ReadSignal` is also [Copy] and `'static`, so it can very easily moved into closures
/// `ReadSignal` is also [`Copy`] and `'static`, so it can very easily moved into closures
/// or copied structs.
///
/// ## Core Trait Implementations
@ -638,7 +637,7 @@ impl<T> SignalWith<T> for ReadSignal<T> {
match with_runtime(self.runtime, |runtime| {
self.id.try_with(runtime, f, diagnostics)
})
.expect("runtime to be alive ")
.expect("runtime to be alive")
{
Ok(o) => o,
Err(_) => panic_getting_dead_signal(
@ -816,13 +815,13 @@ impl<T> Copy for ReadSignal<T> {}
/// and notifies other code when it has changed. This is the
/// core primitive of Leptoss reactive system.
///
/// Calling [WriteSignal::update] will mutate the signals value in place,
/// Calling [`WriteSignal::update`] will mutate the signals value in place,
/// and notify all subscribers that the signals value has changed.
///
/// `WriteSignal` implements [Fn], such that `set_value(new_value)` is equivalent to
/// `WriteSignal` implements [`Fn`], such that `set_value(new_value)` is equivalent to
/// `set_value.update(|value| *value = new_value)`.
///
/// `WriteSignal` is [Copy] and `'static`, so it can very easily moved into closures
/// `WriteSignal` is [`Copy`] and `'static`, so it can very easily moved into closures
/// or copied structs.
///
/// ## Core Trait Implementations
@ -1131,7 +1130,7 @@ pub fn create_rw_signal<T>(cx: Scope, value: T) -> RwSignal<T> {
}
/// A signal that combines the getter and setter into one value, rather than
/// separating them into a [ReadSignal] and a [WriteSignal]. You may prefer this
/// separating them into a [`ReadSignal`] and a [`WriteSignal`]. You may prefer this
/// its style, or it may be easier to pass around in a context or as a function argument.
///
/// ## Core Trait Implementations
@ -1724,7 +1723,7 @@ impl<T> RwSignal<T> {
/// Returns a write-only handle to the signal.
///
/// Useful if you're trying to give write access to another component, or split an
/// `RwSignal` into a [ReadSignal] and a [WriteSignal].
/// [`RwSignal`] into a [`ReadSignal`] and a [`WriteSignal`].
/// ```
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
@ -1873,7 +1872,7 @@ impl NodeId {
runtime.update_if_necessary(*self);
let nodes = runtime.nodes.borrow();
let node = nodes.get(*self).ok_or(SignalError::Disposed)?;
Ok(Rc::clone(&node.value))
Ok(node.value())
}
#[track_caller]
@ -1997,13 +1996,14 @@ impl NodeId {
None
};
// mark descendants dirty
runtime.mark_dirty(*self);
// notify subscribers
if updated.is_some() && !runtime.batching.get() {
Runtime::run_effects(runtime_id);
};
if updated.is_some() {
// mark descendants dirty
runtime.mark_dirty(*self);
runtime.run_effects();
}
updated
})
.unwrap_or_default()

View file

@ -21,8 +21,8 @@ where
}
}
/// A wrapper for any kind of readable reactive signal: a [ReadSignal](crate::ReadSignal),
/// [Memo](crate::Memo), [RwSignal](crate::RwSignal), or derived signal closure.
/// A wrapper for any kind of readable reactive signal: a [`ReadSignal`](crate::ReadSignal),
/// [`Memo`](crate::Memo), [`RwSignal`](crate::RwSignal), or derived signal closure.
///
/// This allows you to create APIs that take any kind of `Signal<T>` as an argument,
/// rather than adding a generic `F: Fn() -> T`. Values can be access with the same

View file

@ -18,8 +18,8 @@ where
}
}
/// A wrapper for any kind of settable reactive signal: a [WriteSignal](crate::WriteSignal),
/// [RwSignal](crate::RwSignal), or closure that receives a value and sets a signal depending
/// A wrapper for any kind of settable reactive signal: a [`WriteSignal`](crate::WriteSignal),
/// [`RwSignal`](crate::RwSignal), or closure that receives a value and sets a signal depending
/// on it.
///
/// This allows you to create APIs that take any kind of `SignalSetter<T>` as an argument,

View file

@ -3,9 +3,9 @@ use crate::{
SignalUpdate, SignalWith,
};
/// Derives a reactive slice of an [RwSignal](crate::RwSignal).
/// Derives a reactive slice of an [`RwSignal`](crate::RwSignal).
///
/// Slices have the same guarantees as [Memos](crate::Memo):
/// Slices have the same guarantees as [`Memo`s](crate::Memo):
/// they only emit their value when it has actually been changed.
///
/// Slices need a getter and a setter, and you must make sure that

View file

@ -2,7 +2,7 @@
use cfg_if::cfg_if;
use std::future::Future;
/// Spawns and runs a thread-local [std::future::Future] in a platform-independent way.
/// Spawns and runs a thread-local [`Future`] in a platform-independent way.
///
/// This can be used to interface with any `async` code.
pub fn spawn_local<F>(fut: F)

View file

@ -7,7 +7,7 @@ use cfg_if::cfg_if;
cfg_if! {
if #[cfg(all(target_arch = "wasm32", any(feature = "csr", feature = "hydrate")))] {
/// Exposes the [queueMicrotask](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask) method
/// Exposes the [`queueMicrotask`](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask) method
/// in the browser, and simply runs the given function when on the server.
pub fn queue_microtask(task: impl FnOnce() + 'static) {
microtask(wasm_bindgen::closure::Closure::once_into_js(task));
@ -21,7 +21,7 @@ cfg_if! {
fn microtask(task: wasm_bindgen::JsValue);
}
} else {
/// Exposes the [queueMicrotask](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask) method
/// Exposes the [`queueMicrotask`](https://developer.mozilla.org/en-US/docs/Web/API/queueMicrotask) method
/// in the browser, and simply runs the given function when on the server.
pub fn queue_microtask(task: impl FnOnce() + 'static) {
task();

View file

@ -3,17 +3,17 @@ use crate::{with_runtime, RuntimeId, Scope, ScopeProperty};
use std::{cell::RefCell, marker::PhantomData, rc::Rc};
slotmap::new_key_type! {
/// Unique ID assigned to a [StoredValue].
/// Unique ID assigned to a [`StoredValue`].
pub(crate) struct StoredValueId;
}
/// A **non-reactive** wrapper for any value, which can be created with [store_value].
/// A **non-reactive** wrapper for any value, which can be created with [`store_value`].
///
/// If you want a reactive wrapper, use [create_signal](crate::create_signal).
/// If you want a reactive wrapper, use [`create_signal`](crate::create_signal).
///
/// This allows you to create a stable reference for any value by storing it within
/// the reactive system. Like the signal types (e.g., [ReadSignal](crate::ReadSignal)
/// and [RwSignal](crate::RwSignal)), it is `Copy` and `'static`. Unlike the signal
/// the reactive system. Like the signal types (e.g., [`ReadSignal`](crate::ReadSignal)
/// and [`RwSignal`](crate::RwSignal)), it is `Copy` and `'static`. Unlike the signal
/// types, it is not reactive; accessing it does not cause effects to subscribe, and
/// updating it does not notify anything else.
#[derive(Debug, PartialEq, Eq, Hash)]
@ -43,7 +43,7 @@ impl<T> StoredValue<T> {
/// to this signal.
///
/// # Panics
/// Panics if you try to access a value stored in a [Scope] that has been disposed.
/// Panics if you try to access a value stored in a [`Scope`] that has been disposed.
///
/// # Examples
/// ```
@ -77,7 +77,7 @@ impl<T> StoredValue<T> {
/// to this signal.
///
/// # Panics
/// Panics if you try to access a value stored in a [Scope] that has been disposed.
/// Panics if you try to access a value stored in a [`Scope`] that has been disposed.
///
/// # Examples
/// ```
@ -128,7 +128,7 @@ impl<T> StoredValue<T> {
/// Applies a function to the current stored value.
///
/// # Panics
/// Panics if you try to access a value stored in a [Scope] that has been disposed.
/// Panics if you try to access a value stored in a [`Scope`] that has been disposed.
///
/// # Examples
/// ```
@ -155,7 +155,7 @@ impl<T> StoredValue<T> {
/// Applies a function to the current stored value.
///
/// # Panics
/// Panics if you try to access a value stored in a [Scope] that has been disposed.
/// Panics if you try to access a value stored in a [`Scope`] that has been disposed.
///
/// # Examples
/// ```
@ -375,8 +375,8 @@ impl<T> StoredValue<T> {
/// Creates a **non-reactive** wrapper for any value by storing it within
/// the reactive system.
///
/// Like the signal types (e.g., [ReadSignal](crate::ReadSignal)
/// and [RwSignal](crate::RwSignal)), it is `Copy` and `'static`. Unlike the signal
/// Like the signal types (e.g., [`ReadSignal`](crate::ReadSignal)
/// and [`RwSignal`](crate::RwSignal)), it is `Copy` and `'static`. Unlike the signal
/// types, it is not reactive; accessing it does not cause effects to subscribe, and
/// updating it does not notify anything else.
/// ```compile_fail

View file

@ -8,7 +8,7 @@ use crate::{
use futures::Future;
use std::{borrow::Cow, collections::VecDeque, pin::Pin};
/// Tracks [Resource](crate::Resource)s that are read under a suspense context,
/// Tracks [`Resource`](crate::Resource)s that are read under a suspense context,
/// i.e., within a [`Suspense`](https://docs.rs/leptos_core/latest/leptos_core/fn.Suspense.html) component.
#[derive(Copy, Clone, Debug)]
pub struct SuspenseContext {

View file

@ -0,0 +1,252 @@
#![forbid(unsafe_code)]
use crate::{
diagnostics,
diagnostics::*,
node::NodeId,
runtime::{with_runtime, RuntimeId},
Scope, ScopeProperty, SignalGet, SignalSet, SignalUpdate,
};
/// Reactive Trigger, notifies reactive code to rerun.
///
/// See [`create_trigger`] for more.
#[derive(Clone, Copy)]
pub struct Trigger {
pub(crate) runtime: RuntimeId,
pub(crate) id: NodeId,
#[cfg(debug_assertions)]
pub(crate) defined_at: &'static std::panic::Location<'static>,
}
impl Trigger {
/// Notifies any reactive code where this trigger is tracked to rerun.
pub fn notify(&self) {
assert!(self.try_notify(), "Trigger::notify(): runtime not alive")
}
/// Attempts to notify any reactive code where this trigger is tracked to rerun.
///
/// Returns `None` if the runtime has been disposed.
pub fn try_notify(&self) -> bool {
with_runtime(self.runtime, |runtime| {
runtime.mark_dirty(self.id);
runtime.run_effects();
})
.is_ok()
}
/// Subscribes the running effect to this trigger.
pub fn track(&self) {
assert!(self.try_track(), "Trigger::track(): runtime not alive")
}
/// Attempts to subscribe the running effect to this trigger, returning
/// `None` if the runtime has been disposed.
pub fn try_track(&self) -> bool {
let diagnostics = diagnostics!(self);
with_runtime(self.runtime, |runtime| {
self.id.subscribe(runtime, diagnostics);
})
.is_ok()
}
}
/// Creates a [`Trigger`], a kind of reactive primitive.
///
/// A trigger is a data-less signal with the sole purpose
/// of notifying other reactive code of a change. This can be useful
/// for when using external data not stored in signals, for example.
///
/// Take a reactive [`Scope`] and returns the [`Trigger`] handle, which
/// can be called as a function to track the trigger in the current
/// reactive context.
///
/// ```
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
/// use std::{cell::RefCell, fmt::Write, rc::Rc};
///
/// let external_data = Rc::new(RefCell::new(1));
/// let output = Rc::new(RefCell::new(String::new()));
///
/// let rerun_on_data = create_trigger(cx);
///
/// let o = output.clone();
/// let e = external_data.clone();
/// create_effect(cx, move |_| {
/// rerun_on_data(); // or rerun_on_data.track();
/// write!(o.borrow_mut(), "{}", *e.borrow());
/// *e.borrow_mut() += 1;
/// });
/// # if !cfg!(feature = "ssr") {
/// assert_eq!(*output.borrow(), "1");
///
/// rerun_on_data.notify(); // reruns the above effect
///
/// assert_eq!(*output.borrow(), "12");
/// # }
/// # }).dispose();
/// ```
#[cfg_attr(
debug_assertions,
instrument(
level = "trace",
skip_all,
fields(scope = ?cx.id)
)
)]
#[track_caller]
pub fn create_trigger(cx: Scope) -> Trigger {
let t = cx.runtime.create_trigger();
cx.push_scope_property(ScopeProperty::Trigger(t.id));
t
}
impl SignalGet<()> for Trigger {
#[cfg_attr(
debug_assertions,
instrument(
level = "trace",
name = "Trigger::get()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at
)
)
)]
#[track_caller]
#[inline(always)]
fn get(&self) {
self.track()
}
#[cfg_attr(
debug_assertions,
instrument(
level = "trace",
name = "Trigger::try_get()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at
)
)
)]
#[inline(always)]
fn try_get(&self) -> Option<()> {
self.try_track().then_some(())
}
}
impl SignalUpdate<()> for Trigger {
#[cfg_attr(
debug_assertions,
instrument(
name = "Trigger::update()",
level = "trace",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at
)
)
)]
#[inline(always)]
fn update(&self, f: impl FnOnce(&mut ())) {
self.try_update(f).expect("runtime to be alive")
}
#[cfg_attr(
debug_assertions,
instrument(
name = "Trigger::try_update()",
level = "trace",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at
)
)
)]
#[inline(always)]
fn try_update<O>(&self, f: impl FnOnce(&mut ()) -> O) -> Option<O> {
// run callback with runtime before dirtying the trigger,
// consistent with signals.
with_runtime(self.runtime, |runtime| {
let res = f(&mut ());
runtime.mark_dirty(self.id);
runtime.run_effects();
Some(res)
})
.ok()
.flatten()
}
}
impl SignalSet<()> for Trigger {
#[cfg_attr(
debug_assertions,
instrument(
level = "trace",
name = "Trigger::set()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at
)
)
)]
#[inline(always)]
fn set(&self, _: ()) {
self.notify();
}
#[cfg_attr(
debug_assertions,
instrument(
level = "trace",
name = "Trigger::try_set()",
skip_all,
fields(
id = ?self.id,
defined_at = %self.defined_at
)
)
)]
#[inline(always)]
fn try_set(&self, _: ()) -> Option<()> {
self.try_notify().then_some(())
}
}
#[cfg(not(feature = "stable"))]
impl FnOnce<()> for Trigger {
type Output = ();
#[inline(always)]
extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
self.track()
}
}
#[cfg(not(feature = "stable"))]
impl FnMut<()> for Trigger {
#[inline(always)]
extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
self.track()
}
}
#[cfg(not(feature = "stable"))]
impl Fn<()> for Trigger {
#[inline(always)]
extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
self.track()
}
}