From f2f52b2533d668f489df11deb473364e3b3a2b73 Mon Sep 17 00:00:00 2001
From: jquesada2016 <54370171+jquesada2016@users.noreply.github.com>
Date: Sat, 18 Feb 2023 06:30:03 -0600
Subject: [PATCH] change: move signal method implementations into traits in
signal prelude (#490)
---
.../hackernews_axum/src/error_template.rs | 32 +-
examples/todomvc/src/storage.rs | 34 +-
integrations/actix/src/lib.rs | 8 +-
integrations/axum/src/lib.rs | 4 +-
leptos/src/error_boundary.rs | 4 +-
leptos/src/show.rs | 2 +-
leptos/src/suspense.rs | 2 +
leptos/tests/ssr.rs | 4 +-
leptos_dom/src/components/errors.rs | 2 +-
leptos_dom/src/node_ref.rs | 4 +-
leptos_macro/src/lib.rs | 2 +-
leptos_reactive/src/lib.rs | 42 +-
leptos_reactive/src/memo.rs | 233 +--
leptos_reactive/src/resource.rs | 4 +-
leptos_reactive/src/runtime.rs | 7 +-
leptos_reactive/src/selector.rs | 3 +-
leptos_reactive/src/signal.rs | 1361 ++++++++++++-----
leptos_reactive/src/signal_wrappers_read.rs | 662 ++++----
leptos_reactive/src/signal_wrappers_write.rs | 71 +-
leptos_reactive/src/slice.rs | 1 +
leptos_reactive/src/stored_value.rs | 285 +++-
leptos_reactive/src/suspense.rs | 5 +-
leptos_reactive/tests/untracked.rs | 2 +-
leptos_server/src/action.rs | 20 +-
leptos_server/src/multi_action.rs | 26 +-
router/src/components/redirect.rs | 4 +-
router/src/components/route.rs | 2 +-
router/src/hooks.rs | 2 +-
28 files changed, 1832 insertions(+), 996 deletions(-)
diff --git a/examples/hackernews_axum/src/error_template.rs b/examples/hackernews_axum/src/error_template.rs
index a8b85d5..57655d3 100644
--- a/examples/hackernews_axum/src/error_template.rs
+++ b/examples/hackernews_axum/src/error_template.rs
@@ -1,4 +1,7 @@
-use leptos::{view, Errors, For, ForProps, IntoView, RwSignal, Scope, View};
+use leptos::{
+ signal_prelude::*, view, Errors, For, ForProps, IntoView, RwSignal, Scope,
+ View,
+};
// A basic function to display errors served by the error boundaries. Feel free to do more complicated things
// here than just displaying them
@@ -6,21 +9,22 @@ pub fn error_template(cx: Scope, errors: Option>) -> View {
let Some(errors) = errors else {
panic!("No Errors found and we expected errors!");
};
+
view! {cx,
-
"Errors"
- "Error: " {error_string}
+
"Errors"
+ "Error: " {error_string}
+ }
}
- }
/>
}
.into_view(cx)
diff --git a/examples/todomvc/src/storage.rs b/examples/todomvc/src/storage.rs
index 3bb54f9..8554da7 100644
--- a/examples/todomvc/src/storage.rs
+++ b/examples/todomvc/src/storage.rs
@@ -1,27 +1,33 @@
use crate::Todo;
-use leptos::Scope;
-use serde::{Deserialize, Serialize};
+use leptos::{
+ signal_prelude::*,
+ Scope,
+};
+use serde::{
+ Deserialize,
+ Serialize,
+};
use uuid::Uuid;
#[derive(Serialize, Deserialize)]
pub struct TodoSerialized {
- pub id: Uuid,
- pub title: String,
- pub completed: bool,
+ pub id: Uuid,
+ pub title: String,
+ pub completed: bool,
}
impl TodoSerialized {
- pub fn into_todo(self, cx: Scope) -> Todo {
- Todo::new_with_completed(cx, self.id, self.title, self.completed)
- }
+ pub fn into_todo(self, cx: Scope) -> Todo {
+ Todo::new_with_completed(cx, self.id, self.title, self.completed)
+ }
}
impl From<&Todo> for TodoSerialized {
- fn from(todo: &Todo) -> Self {
- Self {
- id: todo.id,
- title: todo.title.get(),
- completed: todo.completed.get(),
- }
+ fn from(todo: &Todo) -> Self {
+ Self {
+ id: todo.id,
+ title: todo.title.get(),
+ completed: todo.completed.get(),
}
+ }
}
diff --git a/integrations/actix/src/lib.rs b/integrations/actix/src/lib.rs
index 26e4d60..a8836cc 100644
--- a/integrations/actix/src/lib.rs
+++ b/integrations/actix/src/lib.rs
@@ -282,8 +282,8 @@ pub fn handle_server_fns_with_context(
/// The provides a [MetaContext] and a [RouterIntegrationContext] to app’s context before
/// rendering it, and includes any meta tags injected using [leptos_meta].
///
-/// The HTML stream is rendered using [render_to_stream], and includes everything described in
-/// the documentation for that function.
+/// The HTML stream is rendered using [render_to_stream](leptos::ssr::render_to_stream), and
+/// includes everything described in the documentation for that function.
///
/// This can then be set up at an appropriate route in your application:
/// ```
@@ -602,8 +602,8 @@ where
/// The provides a [MetaContext] and a [RouterIntegrationContext] to app’s context before
/// rendering it, and includes any meta tags injected using [leptos_meta].
///
-/// The HTML stream is rendered using [render_to_stream], and includes everything described in
-/// the documentation for that function.
+/// The HTML stream is rendered using [render_to_stream](leptos::ssr::render_to_stream), and
+/// includes everything described in the documentation for that function.
///
/// This can then be set up at an appropriate route in your application:
/// ```
diff --git a/integrations/axum/src/lib.rs b/integrations/axum/src/lib.rs
index b18157f..9e67447 100644
--- a/integrations/axum/src/lib.rs
+++ b/integrations/axum/src/lib.rs
@@ -342,8 +342,8 @@ pub type PinnedHtmlStream =
/// The provides a [MetaContext] and a [RouterIntegrationContext] to app’s context before
/// rendering it, and includes any meta tags injected using [leptos_meta].
///
-/// The HTML stream is rendered using [render_to_stream], and includes everything described in
-/// the documentation for that function.
+/// The HTML stream is rendered using [render_to_stream](leptos::ssr::render_to_stream), and
+/// includes everything described in the documentation for that function.
///
/// This can then be set up at an appropriate route in your application:
/// ```
diff --git a/leptos/src/error_boundary.rs b/leptos/src/error_boundary.rs
index 344119b..e9639bd 100644
--- a/leptos/src/error_boundary.rs
+++ b/leptos/src/error_boundary.rs
@@ -1,7 +1,9 @@
use crate::Children;
use leptos_dom::{Errors, IntoView};
use leptos_macro::{component, view};
-use leptos_reactive::{create_rw_signal, provide_context, RwSignal, Scope};
+use leptos_reactive::{
+ create_rw_signal, provide_context, signal_prelude::*, RwSignal, Scope,
+};
/// When you render a `Result<_, _>` in your view, in the `Err` case it will
/// render nothing, and search up through the view tree for an ``.
diff --git a/leptos/src/show.rs b/leptos/src/show.rs
index 2c0f830..7ee5026 100644
--- a/leptos/src/show.rs
+++ b/leptos/src/show.rs
@@ -1,6 +1,6 @@
use leptos::component;
use leptos_dom::{Fragment, IntoView};
-use leptos_reactive::{create_memo, Scope};
+use leptos_reactive::{create_memo, signal_prelude::*, Scope};
/// A component that will show its children when the `when` condition is `true`,
/// and show the fallback when it is `false`, without rerendering every time
diff --git a/leptos/src/suspense.rs b/leptos/src/suspense.rs
index f15baf6..dcf68a8 100644
--- a/leptos/src/suspense.rs
+++ b/leptos/src/suspense.rs
@@ -83,6 +83,8 @@ where
fallback().into_view(cx)
}
} else {
+ use leptos_reactive::signal_prelude::*;
+
// run the child; we'll probably throw this away, but it will register resource reads
let child = orig_child(cx).into_view(cx);
let after_original_child = HydrationCtx::id();
diff --git a/leptos/tests/ssr.rs b/leptos/tests/ssr.rs
index 6b66f59..138bc99 100644
--- a/leptos/tests/ssr.rs
+++ b/leptos/tests/ssr.rs
@@ -147,7 +147,7 @@ fn ssr_with_styles() {
use leptos::*;
_ = create_scope(create_runtime(), |cx| {
- let (value, set_value) = create_signal(cx, 0);
+ let (_, set_value) = create_signal(cx, 0);
let styles = "myclass";
let rendered = view! {
cx, class = styles,
@@ -170,7 +170,7 @@ fn ssr_option() {
use leptos::*;
_ = create_scope(create_runtime(), |cx| {
- let (value, set_value) = create_signal(cx, 0);
+ let (_, _) = create_signal(cx, 0);
let rendered = view! {
cx,
diff --git a/leptos_dom/src/components/errors.rs b/leptos_dom/src/components/errors.rs
index 8562f79..4c8983e 100644
--- a/leptos_dom/src/components/errors.rs
+++ b/leptos_dom/src/components/errors.rs
@@ -1,6 +1,6 @@
use crate::{HydrationCtx, IntoView};
use cfg_if::cfg_if;
-use leptos_reactive::{use_context, RwSignal};
+use leptos_reactive::{signal_prelude::*, use_context, RwSignal};
use std::{collections::HashMap, error::Error, sync::Arc};
/// A struct to hold all the possible errors that could be provided by child Views
diff --git a/leptos_dom/src/node_ref.rs b/leptos_dom/src/node_ref.rs
index a9cdbc0..a5be017 100644
--- a/leptos_dom/src/node_ref.rs
+++ b/leptos_dom/src/node_ref.rs
@@ -1,5 +1,7 @@
use crate::{html::ElementDescriptor, HtmlElement};
-use leptos_reactive::{create_effect, create_rw_signal, RwSignal, Scope};
+use leptos_reactive::{
+ create_effect, create_rw_signal, signal_prelude::*, RwSignal, Scope,
+};
use std::cell::Cell;
/// Contains a shared reference to a DOM node created while using the `view`
diff --git a/leptos_macro/src/lib.rs b/leptos_macro/src/lib.rs
index 8cd833a..950dd5a 100644
--- a/leptos_macro/src/lib.rs
+++ b/leptos_macro/src/lib.rs
@@ -347,7 +347,7 @@ pub fn view(tokens: TokenStream) -> TokenStream {
}
/// An optimized, cached template for client-side rendering. Follows the same
-/// syntax as the [view](crate::macro) macro. In hydration or server-side rendering mode,
+/// syntax as the [view!] macro. In hydration or server-side rendering mode,
/// behaves exactly as the `view` macro. In client-side rendering mode, uses a ``
/// node to efficiently render the element. Should only be used with a single root element.
#[proc_macro_error::proc_macro_error]
diff --git a/leptos_reactive/src/lib.rs b/leptos_reactive/src/lib.rs
index 90e6157..8914a20 100644
--- a/leptos_reactive/src/lib.rs
+++ b/leptos_reactive/src/lib.rs
@@ -69,6 +69,8 @@
#[cfg_attr(debug_assertions, macro_use)]
extern crate tracing;
+#[macro_use]
+mod signal;
mod context;
mod effect;
mod hydration;
@@ -78,7 +80,6 @@ mod runtime;
mod scope;
mod selector;
mod serialization;
-mod signal;
mod signal_wrappers_read;
mod signal_wrappers_write;
mod slice;
@@ -96,7 +97,7 @@ pub use runtime::{create_runtime, RuntimeId};
pub use scope::*;
pub use selector::*;
pub use serialization::*;
-pub use signal::*;
+pub use signal::{prelude as signal_prelude, *};
pub use signal_wrappers_read::*;
pub use signal_wrappers_write::*;
pub use slice::*;
@@ -105,43 +106,6 @@ pub use spawn_microtask::*;
pub use stored_value::*;
pub use suspense::SuspenseContext;
-/// Trait implemented for all signal types which you can `get` a value
-/// from, such as [`ReadSignal`],
-/// [`Memo`], etc., which allows getting the inner value without
-/// subscribing to the current scope.
-pub trait UntrackedGettableSignal {
- /// Gets the signal's value without creating a dependency on the
- /// current scope.
- fn get_untracked(&self) -> T
- where
- T: Clone;
-
- /// Runs the provided closure with a reference to the current
- /// value without creating a dependency on the current scope.
- fn with_untracked(&self, f: impl FnOnce(&T) -> O) -> O;
-}
-
-/// Trait implemented for all signal types which you can `set` the inner
-/// value, such as [`WriteSignal`] and [`RwSignal`], which allows setting
-/// the inner value without causing effects which depend on the signal
-/// from being run.
-pub trait UntrackedSettableSignal {
- /// Sets the signal's value without notifying dependents.
- fn set_untracked(&self, new_value: T);
-
- /// Runs the provided closure with a mutable reference to the current
- /// value without notifying dependents.
- fn update_untracked(&self, f: impl FnOnce(&mut T));
-
- /// Runs the provided closure with a mutable reference to the current
- /// value without notifying dependents and returns
- /// the value the closure returned.
- fn update_returning_untracked(
- &self,
- f: impl FnOnce(&mut T) -> U,
- ) -> Option;
-}
-
mod macros {
macro_rules! debug_warn {
($($x:tt)*) => {
diff --git a/leptos_reactive/src/memo.rs b/leptos_reactive/src/memo.rs
index 4257732..9fcee2c 100644
--- a/leptos_reactive/src/memo.rs
+++ b/leptos_reactive/src/memo.rs
@@ -1,5 +1,8 @@
#![forbid(unsafe_code)]
-use crate::{ReadSignal, Scope, SignalError, UntrackedGettableSignal};
+use crate::{
+ create_effect, on_cleanup, ReadSignal, Scope, SignalGet,
+ SignalGetUntracked, SignalStream, SignalWith, SignalWithUntracked,
+};
use std::fmt::Debug;
/// Creates an efficient derived reactive value based on other reactive values.
@@ -91,6 +94,19 @@ where
/// 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
+/// - [`.get()`](#impl-SignalGet-for-Memo) (or calling the signal as a function) clones the current
+/// value of the signal. If you call it within an effect, it will cause that effect
+/// to subscribe to the signal, and to re-run whenever the value of the signal changes.
+/// - [`.get_untracked()`](#impl-SignalGetUntracked-for-Memo) clones the value of the signal
+/// without reactively tracking it.
+/// - [`.with()`](#impl-SignalWith-for-Memo) allows you to reactively access the signal’s value without
+/// cloning by applying a callback function.
+/// - [`.with_untracked()`](#impl-SignalWithUntracked-for-Memo) allows you to access the signal’s
+/// value without reactively tracking it.
+/// - [`.to_stream()`](#impl-SignalStream-for-Memo) converts the signal to an `async` stream of values.
+///
+/// ## Examples
/// ```
/// # use leptos_reactive::*;
/// # fn really_expensive_computation(value: i32) -> i32 { value };
@@ -151,7 +167,7 @@ where
impl Copy for Memo {}
-impl UntrackedGettableSignal for Memo {
+impl SignalGetUntracked for Memo {
#[cfg_attr(
debug_assertions,
instrument(
@@ -165,15 +181,31 @@ impl UntrackedGettableSignal for Memo {
)
)
)]
- fn get_untracked(&self) -> T
- where
- T: Clone,
- {
+ fn get_untracked(&self) -> T {
// Unwrapping is fine because `T` will already be `Some(T)` by
// the time this method can be called
self.0.get_untracked().unwrap()
}
+ #[cfg_attr(
+ debug_assertions,
+ instrument(
+ level = "trace",
+ name = "Memo::try_get_untracked()",
+ skip_all,
+ fields(
+ id = ?self.0.id,
+ defined_at = %self.1,
+ ty = %std::any::type_name::()
+ )
+ )
+ )]
+ fn try_get_untracked(&self) -> Option {
+ self.0.try_get_untracked().flatten()
+ }
+}
+
+impl SignalWithUntracked for Memo {
#[cfg_attr(
debug_assertions,
instrument(
@@ -192,28 +224,42 @@ impl UntrackedGettableSignal for Memo {
// UntrackedSignal>::get_untracked
self.0.with_untracked(|v| f(v.as_ref().unwrap()))
}
+
+ #[cfg_attr(
+ debug_assertions,
+ instrument(
+ level = "trace",
+ name = "Memo::try_with_untracked()",
+ skip_all,
+ fields(
+ id = ?self.0.id,
+ defined_at = %self.1,
+ ty = %std::any::type_name::()
+ )
+ )
+ )]
+ fn try_with_untracked(&self, f: impl FnOnce(&T) -> O) -> Option {
+ self.0.try_with_untracked(|t| f(t.as_ref().unwrap()))
+ }
}
-impl Memo
-where
- T: 'static,
-{
- /// Clones and returns the current value of the memo, and subscribes
- /// the running effect to the memo.
- /// ```
- /// # use leptos_reactive::*;
- /// # create_scope(create_runtime(), |cx| {
- /// let (count, set_count) = create_signal(cx, 0);
- /// let double_count = create_memo(cx, move |_| count() * 2);
- ///
- /// assert_eq!(double_count.get(), 0);
- /// set_count(1);
- ///
- /// // double_count() is shorthand for double_count.get()
- /// assert_eq!(double_count(), 2);
- /// # }).dispose();
- /// #
- /// ```
+/// # Examples
+///
+/// ```
+/// # use leptos_reactive::*;
+/// # create_scope(create_runtime(), |cx| {
+/// let (count, set_count) = create_signal(cx, 0);
+/// let double_count = create_memo(cx, move |_| count() * 2);
+///
+/// assert_eq!(double_count.get(), 0);
+/// set_count(1);
+///
+/// // double_count() is shorthand for double_count.get()
+/// assert_eq!(double_count(), 2);
+/// # }).dispose();
+/// #
+/// ```
+impl SignalGet for Memo {
#[cfg_attr(
debug_assertions,
instrument(
@@ -226,38 +272,15 @@ where
)
)
)]
- pub fn get(&self) -> T
- where
- T: Clone,
- {
- self.with(T::clone)
+ fn get(&self) -> T {
+ self.0.get().unwrap()
}
- /// Applies a function to the current value of the memo, and subscribes
- /// the running effect to this memo.
- /// ```
- /// # use leptos_reactive::*;
- /// # create_scope(create_runtime(), |cx| {
- /// let (name, set_name) = create_signal(cx, "Alice".to_string());
- /// let name_upper = create_memo(cx, move |_| name().to_uppercase());
- ///
- /// // ❌ unnecessarily clones the string
- /// let first_char = move || name_upper().chars().next().unwrap();
- /// assert_eq!(first_char(), 'A');
- ///
- /// // ✅ gets the first char without cloning the `String`
- /// let first_char = move || name_upper.with(|n| n.chars().next().unwrap());
- /// assert_eq!(first_char(), 'A');
- /// set_name("Bob".to_string());
- /// assert_eq!(first_char(), 'B');
- /// # }).dispose();
- /// #
- /// ```
#[cfg_attr(
debug_assertions,
instrument(
- name = "Memo::with()",
level = "trace",
+ name = "Memo::try_get()",
skip_all,
fields(
id = ?self.0.id,
@@ -266,56 +289,76 @@ where
)
)
)]
- pub fn with(&self, f: impl FnOnce(&T) -> U) -> U {
- // okay to unwrap here, because the value will *always* have initially
- // been set by the effect, synchronously
- self.0
- .with(|n| f(n.as_ref().expect("Memo is missing its initial value")))
+ fn try_get(&self) -> Option {
+ self.0.try_get().flatten()
+ }
+}
+
+impl SignalWith for Memo {
+ #[cfg_attr(
+ debug_assertions,
+ instrument(
+ level = "trace",
+ name = "Memo::with()",
+ skip_all,
+ fields(
+ id = ?self.0.id,
+ defined_at = %self.1,
+ ty = %std::any::type_name::()
+ )
+ )
+ )]
+ fn with(&self, f: impl FnOnce(&T) -> O) -> O {
+ self.0.with(|t| f(t.as_ref().unwrap()))
}
- pub(crate) fn try_with(
+ #[cfg_attr(
+ debug_assertions,
+ instrument(
+ level = "trace",
+ name = "Memo::try_with()",
+ skip_all,
+ fields(
+ id = ?self.0.id,
+ defined_at = %self.1,
+ ty = %std::any::type_name::()
+ )
+ )
+ )]
+ fn try_with(&self, f: impl FnOnce(&T) -> O) -> Option {
+ self.0.try_with(|t| f(t.as_ref().unwrap())).ok()
+ }
+}
+
+impl SignalStream for Memo {
+ fn to_stream(
&self,
- f: impl Fn(&T) -> U,
- ) -> Result {
- self.0.try_with(|n| {
- f(n.as_ref().expect("Memo is missing its initial value"))
- })
- }
+ cx: Scope,
+ ) -> std::pin::Pin>> {
+ let (tx, rx) = futures::channel::mpsc::unbounded();
+ let close_channel = tx.clone();
+
+ on_cleanup(cx, move || close_channel.close_channel());
+
+ let this = *self;
+
+ create_effect(cx, move |_| {
+ let _ = tx.unbounded_send(this.get());
+ });
+
+ Box::pin(rx)
+ }
+}
+
+impl Memo
+where
+ T: 'static,
+{
#[cfg(feature = "hydrate")]
pub(crate) fn subscribe(&self) {
self.0.subscribe()
}
}
-#[cfg(not(feature = "stable"))]
-impl FnOnce<()> for Memo
-where
- T: Clone,
-{
- type Output = T;
-
- extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
- self.get()
- }
-}
-
-#[cfg(not(feature = "stable"))]
-impl FnMut<()> for Memo
-where
- T: Clone,
-{
- extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
- self.get()
- }
-}
-
-#[cfg(not(feature = "stable"))]
-impl Fn<()> for Memo
-where
- T: Clone,
-{
- extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
- self.get()
- }
-}
+impl_get_fn_traits![Memo];
diff --git a/leptos_reactive/src/resource.rs b/leptos_reactive/src/resource.rs
index b5c92f0..75ce565 100644
--- a/leptos_reactive/src/resource.rs
+++ b/leptos_reactive/src/resource.rs
@@ -5,8 +5,8 @@ use crate::{
runtime::{with_runtime, RuntimeId},
serialization::Serializable,
spawn::spawn_local,
- use_context, Memo, ReadSignal, Scope, ScopeProperty, SuspenseContext,
- WriteSignal,
+ use_context, Memo, ReadSignal, Scope, ScopeProperty, SignalUpdate,
+ SignalWith, SuspenseContext, WriteSignal,
};
use std::{
any::Any,
diff --git a/leptos_reactive/src/runtime.rs b/leptos_reactive/src/runtime.rs
index 6bf99cf..768d81f 100644
--- a/leptos_reactive/src/runtime.rs
+++ b/leptos_reactive/src/runtime.rs
@@ -2,7 +2,7 @@
use crate::{
hydration::SharedContext, AnyEffect, AnyResource, Effect, EffectId, Memo,
ReadSignal, ResourceId, ResourceState, RwSignal, Scope, ScopeDisposer,
- ScopeId, ScopeProperty, SerializableResource, SignalId,
+ ScopeId, ScopeProperty, SerializableResource, SignalId, SignalUpdate,
UnserializableResource, WriteSignal,
};
use cfg_if::cfg_if;
@@ -394,7 +394,7 @@ impl Runtime {
}
}
- /// Returns IDs for all [Resource]s found on any scope.
+ /// Returns IDs for all [resources](crate::Resource) found on any scope.
pub(crate) fn all_resources(&self) -> Vec {
self.resources
.borrow()
@@ -403,7 +403,8 @@ impl Runtime {
.collect()
}
- /// Returns IDs for all [Resource]s found on any scope, pending from the server.
+ /// Returns IDs for all [resources](crate::Resource) found on any
+ /// scope, pending from the server.
pub(crate) fn pending_resources(&self) -> Vec {
self.resources
.borrow()
diff --git a/leptos_reactive/src/selector.rs b/leptos_reactive/src/selector.rs
index d588026..3143aa9 100644
--- a/leptos_reactive/src/selector.rs
+++ b/leptos_reactive/src/selector.rs
@@ -1,6 +1,7 @@
#![forbid(unsafe_code)]
use crate::{
- create_isomorphic_effect, create_signal, ReadSignal, Scope, WriteSignal,
+ create_isomorphic_effect, create_signal, ReadSignal, Scope, SignalUpdate,
+ WriteSignal,
};
use std::{
cell::RefCell, collections::HashMap, fmt::Debug, hash::Hash, rc::Rc,
diff --git a/leptos_reactive/src/signal.rs b/leptos_reactive/src/signal.rs
index add02f3..6c5caa7 100644
--- a/leptos_reactive/src/signal.rs
+++ b/leptos_reactive/src/signal.rs
@@ -1,15 +1,273 @@
#![forbid(unsafe_code)]
use crate::{
+ console_warn, create_effect,
macros::debug_warn,
+ on_cleanup,
runtime::{with_runtime, RuntimeId},
- Runtime, Scope, ScopeProperty, UntrackedGettableSignal,
- UntrackedSettableSignal,
+ Runtime, Scope, ScopeProperty,
};
use cfg_if::cfg_if;
use futures::Stream;
-use std::{fmt::Debug, marker::PhantomData};
+use std::{fmt::Debug, marker::PhantomData, pin::Pin};
use thiserror::Error;
+macro_rules! impl_get_fn_traits {
+ ($($ty:ident $(($method_name:ident))?),*) => {
+ $(
+ #[cfg(not(feature = "stable"))]
+ impl FnOnce<()> for $ty {
+ type Output = T;
+
+ extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
+ impl_get_fn_traits!(@method_name self $($method_name)?)
+ }
+ }
+
+ #[cfg(not(feature = "stable"))]
+ impl FnMut<()> for $ty {
+ extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
+ impl_get_fn_traits!(@method_name self $($method_name)?)
+ }
+ }
+
+ #[cfg(not(feature = "stable"))]
+ impl Fn<()> for $ty {
+ extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
+ impl_get_fn_traits!(@method_name self $($method_name)?)
+ }
+ }
+ )*
+ };
+ (@method_name $self:ident) => {
+ $self.get()
+ };
+ (@method_name $self:ident $ident:ident) => {
+ $self.$ident()
+ };
+}
+
+macro_rules! impl_set_fn_traits {
+ ($($ty:ident $($method_name:ident)?),*) => {
+ $(
+ #[cfg(not(feature = "stable"))]
+ impl FnOnce<(T,)> for $ty {
+ type Output = ();
+
+ extern "rust-call" fn call_once(self, args: (T,)) -> Self::Output {
+ impl_set_fn_traits!(@method_name self $($method_name)? args)
+ }
+ }
+
+ #[cfg(not(feature = "stable"))]
+ impl FnMut<(T,)> for $ty {
+ extern "rust-call" fn call_mut(&mut self, args: (T,)) -> Self::Output {
+ impl_set_fn_traits!(@method_name self $($method_name)? args)
+ }
+ }
+
+ #[cfg(not(feature = "stable"))]
+ impl Fn<(T,)> for $ty {
+ extern "rust-call" fn call(&self, args: (T,)) -> Self::Output {
+ impl_set_fn_traits!(@method_name self $($method_name)? args)
+ }
+ }
+ )*
+ };
+ (@method_name $self:ident $args:ident) => {
+ $self.set($args.0)
+ };
+ (@method_name $self:ident $ident:ident $args:ident) => {
+ $self.$ident($args.0)
+ };
+}
+
+impl_get_fn_traits![ReadSignal, RwSignal];
+impl_set_fn_traits![WriteSignal];
+
+/// This prelude imports all signal types as well as all signal
+/// traits needed to use those types.
+pub mod prelude {
+ pub use super::*;
+ pub use crate::{
+ memo::*, selector::*, signal_wrappers_read::*, signal_wrappers_write::*,
+ };
+}
+
+/// This trait allows getting an owned value of the signals
+/// inner type.
+pub trait SignalGet {
+ /// Clones and returns the current value of the signal, and subscribes
+ /// 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.
+ #[track_caller]
+ fn get(&self) -> T;
+
+ /// Clones and returns the signal value, returning [`Some`] if the signal
+ /// is still alive, and [`None`] otherwise.
+ fn try_get(&self) -> Option;
+}
+
+/// This trait allows obtaining an immutable reference to the signal's
+/// inner type.
+pub trait SignalWith {
+ /// Applies a function to the current value of the signal, and subscribes
+ /// 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.
+ #[track_caller]
+ fn with(&self, f: impl FnOnce(&T) -> O) -> O;
+
+ /// Applies a function to the current value of the signal, and subscribes
+ /// the running effect to this signal. Returns [`Some`] if the signal is
+ /// valid and the function ran, otherwise returns [`None`].
+ fn try_with(&self, f: impl FnOnce(&T) -> O) -> Option;
+}
+
+/// This trait allows setting the value of a signal.
+pub trait SignalSet {
+ /// Sets the signal’s value and notifies subscribers.
+ ///
+ /// **Note:** `set()` does not auto-memoize, i.e., it will notify subscribers
+ /// even if the value has not actually changed.
+ #[track_caller]
+ fn set(&self, new_value: T);
+
+ /// Sets the signal’s value and notifies subscribers. Returns [`None`]
+ /// if the signal is still valid, [`Some(T)`] otherwise.
+ ///
+ /// **Note:** `set()` does not auto-memoize, i.e., it will notify subscribers
+ /// even if the value has not actually changed.
+ fn try_set(&self, new_value: T) -> Option;
+}
+
+/// This trait allows updating the inner value of a signal.
+pub trait SignalUpdate {
+ /// Applies a function to the current value to mutate it in place
+ /// and notifies subscribers that the signal has changed.
+ ///
+ /// **Note:** `update()` does not auto-memoize, i.e., it will notify subscribers
+ /// even if the value has not actually changed.
+ #[track_caller]
+ fn update(&self, f: impl FnOnce(&mut T));
+
+ /// Applies a function to the current value to mutate it in place
+ /// and notifies subscribers that the signal has changed. Returns
+ /// [`Some(O)`] if the signal is still valid, [`None`] otherwise.
+ ///
+ /// **Note:** `update()` does not auto-memoize, i.e., it will notify subscribers
+ /// even if the value has not actually changed.
+ #[deprecated = "Please use `try_update` instead. This method will be \
+ removed in a future version of this crate"]
+ fn update_returning(&self, f: impl FnOnce(&mut T) -> O) -> Option {
+ self.try_update(f)
+ }
+
+ /// Applies a function to the current value to mutate it in place
+ /// and notifies subscribers that the signal has changed. Returns
+ /// [`Some(O)`] if the signal is still valid, [`None`] otherwise.
+ ///
+ /// **Note:** `update()` does not auto-memoize, i.e., it will notify subscribers
+ /// even if the value has not actually changed.
+ fn try_update(&self, f: impl FnOnce(&mut T) -> O) -> Option;
+}
+
+/// Trait implemented for all signal types which you can `get` a value
+/// from, such as [`ReadSignal`],
+/// [`Memo`](crate::Memo), etc., which allows getting the inner value without
+/// subscribing to the current scope.
+pub trait SignalGetUntracked {
+ /// Gets the signal's 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.
+ #[track_caller]
+ fn get_untracked(&self) -> T;
+
+ /// Gets the signal's value without creating a dependency on the
+ /// current scope. Returns [`Some(T)`] if the signal is still
+ /// valid, [`None`] otherwise.
+ fn try_get_untracked(&self) -> Option;
+}
+
+/// This trait allows getting a reference to the signals inner value
+/// without creating a dependency on the signal.
+pub trait SignalWithUntracked {
+ /// Runs the provided closure with a reference to the current
+ /// 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.
+ #[track_caller]
+ fn with_untracked(&self, f: impl FnOnce(&T) -> O) -> O;
+
+ /// Runs the provided closure with a reference to the current
+ /// value without creating a dependency on the current scope.
+ /// Returns [`Some(O)`] if the signal is still valid, [`None`]
+ /// otherwise.
+ #[track_caller]
+ fn try_with_untracked(&self, f: impl FnOnce(&T) -> O) -> Option;
+}
+
+/// Trait implemented for all signal types which you can `set` the inner
+/// value, such as [`WriteSignal`] and [`RwSignal`], which allows setting
+/// the inner value without causing effects which depend on the signal
+/// from being run.
+pub trait SignalSetUntracked {
+ /// Sets the signal's value without notifying dependents.
+ #[track_caller]
+ fn set_untracked(&self, new_value: T);
+
+ /// Attempts to set the signal if it's still valid. Returns [`None`]
+ /// if the signal was set, [`Some(T)`] otherwise.
+ #[track_caller]
+ fn try_set_untracked(&self, new_value: T) -> Option;
+}
+
+/// This trait allows updating the signals value without causing
+/// dependant effects to run.
+pub trait SignalUpdateUntracked {
+ /// Runs the provided closure with a mutable reference to the current
+ /// value without notifying dependents.
+ #[track_caller]
+ fn update_untracked(&self, f: impl FnOnce(&mut T));
+
+ /// Runs the provided closure with a mutable reference to the current
+ /// value without notifying dependents and returns
+ /// the value the closure returned.
+ #[deprecated = "Please use `try_update_untracked` instead. This method \
+ will be removed in a future version of `leptos`"]
+ fn update_returning_untracked(
+ &self,
+ f: impl FnOnce(&mut T) -> U,
+ ) -> Option {
+ self.try_update_untracked(f)
+ }
+
+ /// Runs the provided closure with a mutable reference to the current
+ /// value without notifying dependents and returns
+ /// the value the closure returned.
+ fn try_update_untracked(&self, f: impl FnOnce(&mut T) -> O)
+ -> Option;
+}
+
+/// This trait allows converting a signal into a async [`Stream`].
+pub trait SignalStream {
+ /// Generates a [`Stream`] that emits the new value of the signal
+ /// whenever it changes.
+ ///
+ /// # Panics
+ /// 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
+ #[track_caller]
+ fn to_stream(&self, cx: Scope) -> Pin>>;
+}
+
/// Creates a signal, the basic reactive primitive.
///
/// A signal is a piece of data that may change over time,
@@ -161,15 +419,22 @@ pub fn create_signal_from_stream(
/// and notifies other code when it has changed. This is the
/// core primitive of Leptos’s reactive system.
///
-/// Calling [ReadSignal::get] within an effect will cause that effect
-/// to subscribe to the signal, and to re-run whenever the value of
-/// the signal changes.
-///
-/// `ReadSignal` implements [Fn], so that `value()` and `value.get()` are identical.
-///
/// `ReadSignal` is also [Copy] and `'static`, so it can very easily moved into closures
/// or copied structs.
///
+/// ## Core Trait Implementations
+/// - [`.get()`](#impl-SignalGet-for-ReadSignal) (or calling the signal as a function) clones the current
+/// value of the signal. If you call it within an effect, it will cause that effect
+/// to subscribe to the signal, and to re-run whenever the value of the signal changes.
+/// - [`.get_untracked()`](#impl-SignalGetUntracked-for-ReadSignal) clones the value of the signal
+/// without reactively tracking it.
+/// - [`.with()`](#impl-SignalWith-for-ReadSignal) allows you to reactively access the signal’s value without
+/// cloning by applying a callback function.
+/// - [`.with_untracked()`](#impl-SignalWithUntracked-for-ReadSignal) allows you to access the signal’s
+/// value without reactively tracking it.
+/// - [`.to_stream()`](#impl-SignalStream-for-ReadSignal) converts the signal to an `async` stream of values.
+///
+/// # Examples
/// ```
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
@@ -210,7 +475,7 @@ where
pub(crate) defined_at: &'static std::panic::Location<'static>,
}
-impl UntrackedGettableSignal for ReadSignal {
+impl SignalGetUntracked for ReadSignal {
#[cfg_attr(
debug_assertions,
instrument(
@@ -224,13 +489,43 @@ impl UntrackedGettableSignal for ReadSignal {
)
)
)]
- fn get_untracked(&self) -> T
- where
- T: Clone,
- {
- self.with_no_subscription(|v| v.clone())
+ fn get_untracked(&self) -> T {
+ match with_runtime(self.runtime, |runtime| {
+ self.id.try_with_no_subscription(runtime, T::clone)
+ })
+ .expect("runtime to be alive")
+ {
+ Ok(t) => t,
+ Err(_) => panic_getting_dead_signal(
+ #[cfg(debug_assertions)]
+ self.defined_at,
+ ),
+ }
}
+ #[cfg_attr(
+ debug_assertions,
+ instrument(
+ level = "trace",
+ name = "ReadSignal::try_get_untracked()",
+ skip_all,
+ fields(
+ id = ?self.id,
+ defined_at = %self.defined_at,
+ ty = %std::any::type_name::()
+ )
+ )
+ )]
+ fn try_get_untracked(&self) -> Option {
+ with_runtime(self.runtime, |runtime| {
+ self.id.try_with_no_subscription(runtime, Clone::clone).ok()
+ })
+ .ok()
+ .flatten()
+ }
+}
+
+impl SignalWithUntracked for ReadSignal {
#[cfg_attr(
debug_assertions,
instrument(
@@ -247,30 +542,48 @@ impl UntrackedGettableSignal for ReadSignal {
fn with_untracked(&self, f: impl FnOnce(&T) -> O) -> O {
self.with_no_subscription(f)
}
+
+ #[cfg_attr(
+ debug_assertions,
+ instrument(
+ level = "trace",
+ name = "ReadSignal::try_with_untracked()",
+ skip_all,
+ fields(
+ id = ?self.id,
+ defined_at = %self.defined_at,
+ ty = %std::any::type_name::()
+ )
+ )
+ )]
+ fn try_with_untracked(&self, f: impl FnOnce(&T) -> O) -> Option {
+ with_runtime(self.runtime, |runtime| self.id.try_with(runtime, f))
+ .ok()
+ .transpose()
+ .ok()
+ .flatten()
+ }
}
-impl ReadSignal
-where
- T: 'static,
-{
- /// Applies a function to the current value of the signal, and subscribes
- /// the running effect to this signal.
- /// ```
- /// # use leptos_reactive::*;
- /// # create_scope(create_runtime(), |cx| {
- /// let (name, set_name) = create_signal(cx, "Alice".to_string());
- ///
- /// // ❌ unnecessarily clones the string
- /// let first_char = move || name().chars().next().unwrap();
- /// assert_eq!(first_char(), 'A');
- ///
- /// // ✅ gets the first char without cloning the `String`
- /// let first_char = move || name.with(|n| n.chars().next().unwrap());
- /// assert_eq!(first_char(), 'A');
- /// set_name("Bob".to_string());
- /// assert_eq!(first_char(), 'B');
- /// });
- /// ```
+/// # Examples
+///
+/// ```
+/// # use leptos_reactive::*;
+/// # create_scope(create_runtime(), |cx| {
+/// let (name, set_name) = create_signal(cx, "Alice".to_string());
+///
+/// // ❌ unnecessarily clones the string
+/// let first_char = move || name().chars().next().unwrap();
+/// assert_eq!(first_char(), 'A');
+///
+/// // ✅ gets the first char without cloning the `String`
+/// let first_char = move || name.with(|n| n.chars().next().unwrap());
+/// assert_eq!(first_char(), 'A');
+/// set_name("Bob".to_string());
+/// assert_eq!(first_char(), 'B');
+/// # });
+/// ```
+impl SignalWith for ReadSignal {
#[cfg_attr(
debug_assertions,
instrument(
@@ -284,33 +597,52 @@ where
)
)
)]
- pub fn with(&self, f: impl FnOnce(&T) -> U) -> U {
- self.id.with(self.runtime, f)
+ fn with(&self, f: impl FnOnce(&T) -> O) -> O {
+ match with_runtime(self.runtime, |runtime| self.id.try_with(runtime, f))
+ .expect("runtime to be alive ")
+ {
+ Ok(o) => o,
+ Err(_) => panic_getting_dead_signal(
+ #[cfg(debug_assertions)]
+ self.defined_at,
+ ),
+ }
}
- pub(crate) fn with_no_subscription(&self, f: impl FnOnce(&T) -> U) -> U {
- self.id.with_no_subscription(self.runtime, f)
+ #[cfg_attr(
+ debug_assertions,
+ instrument(
+ level = "trace",
+ name = "ReadSignal::try_with()",
+ skip_all,
+ fields(
+ id = ?self.id,
+ defined_at = %self.defined_at,
+ ty = %std::any::type_name::()
+ )
+ )
+ )]
+ fn try_with(&self, f: impl FnOnce(&T) -> O) -> Option {
+ with_runtime(self.runtime, |runtime| self.id.try_with(runtime, f).ok())
+ .ok()
+ .flatten()
}
+}
- #[cfg(feature = "hydrate")]
- pub(crate) fn subscribe(&self) {
- _ = with_runtime(self.runtime, |runtime| self.id.subscribe(runtime))
- }
-
- /// Clones and returns the current value of the signal, and subscribes
- /// the running effect to this signal.
- ///
- /// If you want to get the value without cloning it, use [ReadSignal::with].
- /// (`value.get()` is equivalent to `value.with(T::clone)`.)
- /// ```
- /// # use leptos_reactive::*;
- /// # create_scope(create_runtime(), |cx| {
- /// let (count, set_count) = create_signal(cx, 0);
- ///
- /// // calling the getter clones and returns the value
- /// assert_eq!(count(), 0);
- /// });
- /// ```
+/// # Examples
+///
+/// ```
+/// # use leptos_reactive::*;
+/// # create_scope(create_runtime(), |cx| {
+/// let (count, set_count) = create_signal(cx, 0);
+///
+/// assert_eq!(count.get(), 0);
+///
+/// // count() is shorthand for count.get()
+/// assert_eq!(count(), 0);
+/// # });
+/// ```
+impl SignalGet for ReadSignal {
#[cfg_attr(
debug_assertions,
instrument(
@@ -324,11 +656,80 @@ where
)
)
)]
- pub fn get(&self) -> T
- where
- T: Clone,
- {
- self.id.with(self.runtime, T::clone)
+ fn get(&self) -> T {
+ match with_runtime(self.runtime, |runtime| {
+ self.id.try_with(runtime, T::clone)
+ })
+ .expect("runtime to be alive")
+ {
+ Ok(t) => t,
+ Err(_) => panic_getting_dead_signal(
+ #[cfg(debug_assertions)]
+ self.defined_at,
+ ),
+ }
+ }
+
+ #[cfg_attr(
+ debug_assertions,
+ instrument(
+ level = "trace",
+ name = "ReadSignal::try_get()",
+ skip_all,
+ fields(
+ id = ?self.id,
+ defined_at = %self.defined_at,
+ ty = %std::any::type_name::()
+ )
+ )
+ )]
+ fn try_get(&self) -> Option {
+ self.try_with(Clone::clone).ok()
+ }
+}
+
+impl SignalStream for ReadSignal {
+ #[cfg_attr(
+ debug_assertions,
+ instrument(
+ level = "trace",
+ name = "ReadSignal::to_stream()",
+ skip_all,
+ fields(
+ id = ?self.id,
+ defined_at = %self.defined_at,
+ ty = %std::any::type_name::()
+ )
+ )
+ )]
+ fn to_stream(&self, cx: Scope) -> Pin>> {
+ let (tx, rx) = futures::channel::mpsc::unbounded();
+
+ let close_channel = tx.clone();
+
+ on_cleanup(cx, move || close_channel.close_channel());
+
+ let this = *self;
+
+ create_effect(cx, move |_| {
+ let _ = tx.unbounded_send(this.get());
+ });
+
+ Box::pin(rx)
+ }
+}
+
+impl ReadSignal
+where
+ T: 'static,
+{
+ pub(crate) fn with_no_subscription(&self, f: impl FnOnce(&T) -> U) -> U {
+ self.id.with_no_subscription(self.runtime, f)
+ }
+
+ #[cfg(feature = "hydrate")]
+ pub(crate) fn subscribe(&self) {
+ _ = with_runtime(self.runtime, |runtime| self.id.subscribe(runtime))
}
/// Applies the function to the current Signal, if it exists, and subscribes
@@ -344,22 +745,6 @@ where
Err(_) => Err(SignalError::RuntimeDisposed),
}
}
-
- /// Generates a [Stream](futures::stream::Stream) that emits the new value of the signal
- /// whenever it changes.
- pub fn to_stream(&self) -> impl Stream
- where
- T: Clone,
- {
- let (tx, rx) = futures::channel::mpsc::unbounded();
- let id = self.id;
- let runtime = self.runtime;
- // TODO: because it's not attached to a scope, this effect will leak if the scope is disposed
- runtime.create_effect(move |_| {
- _ = tx.unbounded_send(id.with(runtime, T::clone));
- });
- rx
- }
}
impl Clone for ReadSignal {
@@ -376,38 +761,6 @@ impl Clone for ReadSignal {
impl Copy for ReadSignal {}
-#[cfg(not(feature = "stable"))]
-impl FnOnce<()> for ReadSignal
-where
- T: Clone,
-{
- type Output = T;
-
- extern "rust-call" fn call_once(self, _args: ()) -> Self::Output {
- self.get()
- }
-}
-
-#[cfg(not(feature = "stable"))]
-impl FnMut<()> for ReadSignal
-where
- T: Clone,
-{
- extern "rust-call" fn call_mut(&mut self, _args: ()) -> Self::Output {
- self.get()
- }
-}
-
-#[cfg(not(feature = "stable"))]
-impl Fn<()> for ReadSignal
-where
- T: Clone,
-{
- extern "rust-call" fn call(&self, _args: ()) -> Self::Output {
- self.get()
- }
-}
-
/// The setter for a reactive signal.
///
/// A signal is a piece of data that may change over time,
@@ -423,6 +776,18 @@ where
/// `WriteSignal` is [Copy] and `'static`, so it can very easily moved into closures
/// or copied structs.
///
+/// ## Core Trait Implementations
+/// - [`.set()`](#impl-SignalSet-for-WriteSignal) (or calling the setter as a function)
+/// sets the signal’s value, and notifies all subscribers that the signal’s value has changed.
+/// to subscribe to the signal, and to re-run whenever the value of the signal changes.
+/// - [`.set_untracked()`](#impl-SignalSetUntracked-for-WriteSignal) sets the signal’s value
+/// without notifying its subscribers.
+/// - [`.update()`](#impl-SignalUpdate-for-WriteSignal) mutates the signal’s value in place
+/// and notifies all subscribers that the signal’s value has changed.
+/// - [`.update_untracked()`](#impl-SignalUpdateUntracked-for-WriteSignal) mutates the signal’s value
+/// in place without notifying its subscribers.
+///
+/// ## Examples
/// ```
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
@@ -453,7 +818,7 @@ where
pub(crate) defined_at: &'static std::panic::Location<'static>,
}
-impl UntrackedSettableSignal for WriteSignal
+impl SignalSetUntracked for WriteSignal
where
T: 'static,
{
@@ -475,6 +840,30 @@ where
.update_with_no_effect(self.runtime, |v| *v = new_value);
}
+ #[cfg_attr(
+ debug_assertions,
+ instrument(
+ level = "trace",
+ name = "WriteSignal::try_set_untracked()",
+ skip_all,
+ fields(
+ id = ?self.id,
+ defined_at = %self.defined_at,
+ ty = %std::any::type_name::()
+ )
+ )
+ )]
+ fn try_set_untracked(&self, new_value: T) -> Option {
+ let mut new_value = Some(new_value);
+
+ self.id
+ .update(self.runtime, |t| *t = new_value.take().unwrap());
+
+ new_value
+ }
+}
+
+impl SignalUpdateUntracked for WriteSignal {
#[cfg_attr(
debug_assertions,
instrument(
@@ -511,32 +900,32 @@ where
) -> Option {
self.id.update_with_no_effect(self.runtime, f)
}
+
+ fn try_update_untracked(
+ &self,
+ f: impl FnOnce(&mut T) -> O,
+ ) -> Option {
+ self.id.update_with_no_effect(self.runtime, f)
+ }
}
-impl WriteSignal
-where
- T: 'static,
-{
- /// Applies a function to the current value to mutate it in place
- /// and notifies subscribers that the signal has changed.
- ///
- /// **Note:** `update()` does not auto-memoize, i.e., it will notify subscribers
- /// even if the value has not actually changed.
- /// ```
- /// # use leptos_reactive::*;
- /// # create_scope(create_runtime(), |cx| {
- /// let (count, set_count) = create_signal(cx, 0);
- ///
- /// // notifies subscribers
- /// set_count.update(|n| *n = 1); // it's easier just to call set_count(1), though!
- /// assert_eq!(count(), 1);
- ///
- /// // you can include arbitrary logic in this update function
- /// // also notifies subscribers, even though the value hasn't changed
- /// set_count.update(|n| if *n > 3 { *n += 1 });
- /// assert_eq!(count(), 1);
- /// # }).dispose();
- /// ```
+/// # Examples
+/// ```
+/// # use leptos_reactive::*;
+/// # create_scope(create_runtime(), |cx| {
+/// let (count, set_count) = create_signal(cx, 0);
+///
+/// // notifies subscribers
+/// set_count.update(|n| *n = 1); // it's easier just to call set_count(1), though!
+/// assert_eq!(count(), 1);
+///
+/// // you can include arbitrary logic in this update function
+/// // also notifies subscribers, even though the value hasn't changed
+/// set_count.update(|n| if *n > 3 { *n += 1 });
+/// assert_eq!(count(), 1);
+/// # }).dispose();
+/// ```
+impl SignalUpdate for WriteSignal {
#[cfg_attr(
debug_assertions,
instrument(
@@ -550,42 +939,20 @@ where
)
)
)]
- pub fn update(&self, f: impl FnOnce(&mut T)) {
- self.id.update(self.runtime, f);
+ fn update(&self, f: impl FnOnce(&mut T)) {
+ if self.id.update(self.runtime, f).is_none() {
+ warn_updating_dead_signal(
+ #[cfg(debug_assertions)]
+ self.defined_at,
+ );
+ }
}
- /// Applies a function to the current value to mutate it in place
- /// and notifies subscribers that the signal has changed.
- /// Forwards the return value of the closure if the closure was called
- ///
- /// **Note:** `update()` does not auto-memoize, i.e., it will notify subscribers
- /// even if the value has not actually changed.
- /// ```
- /// # use leptos_reactive::*;
- /// # create_scope(create_runtime(), |cx| {
- /// let (count, set_count) = create_signal(cx, 0);
- ///
- /// // notifies subscribers
- /// let value = set_count.update_returning(|n| {
- /// *n = 1;
- /// *n * 10
- /// });
- /// assert_eq!(value, Some(10));
- /// assert_eq!(count(), 1);
- ///
- /// let value = set_count.update_returning(|n| {
- /// *n += 1;
- /// *n * 10
- /// });
- /// assert_eq!(value, Some(20));
- /// assert_eq!(count(), 2);
- /// # }).dispose();
- /// ```
#[cfg_attr(
debug_assertions,
instrument(
+ name = "WriteSignal::try_update()",
level = "trace",
- name = "WriteSignal::update_returning()"
skip_all,
fields(
id = ?self.id,
@@ -594,32 +961,29 @@ where
)
)
)]
- pub fn update_returning(
- &self,
- f: impl FnOnce(&mut T) -> U,
- ) -> Option {
+ fn try_update(&self, f: impl FnOnce(&mut T) -> O) -> Option {
self.id.update(self.runtime, f)
}
+}
- /// Sets the signal’s value and notifies subscribers.
- ///
- /// **Note:** `set()` does not auto-memoize, i.e., it will notify subscribers
- /// even if the value has not actually changed.
- /// ```
- /// # use leptos_reactive::*;
- /// # create_scope(create_runtime(), |cx| {
- /// let (count, set_count) = create_signal(cx, 0);
- ///
- /// // notifies subscribers
- /// set_count.update(|n| *n = 1); // it's easier just to call set_count(1), though!
- /// assert_eq!(count(), 1);
- ///
- /// // you can include arbitrary logic in this update function
- /// // also notifies subscribers, even though the value hasn't changed
- /// set_count.update(|n| if *n > 3 { *n += 1 });
- /// assert_eq!(count(), 1);
- /// # }).dispose();
- /// ```
+/// # Examples
+///
+/// ```
+/// # use leptos_reactive::*;
+/// # create_scope(create_runtime(), |cx| {
+/// let (count, set_count) = create_signal(cx, 0);
+///
+/// // notifies subscribers
+/// set_count.update(|n| *n = 1); // it's easier just to call set_count(1), though!
+/// assert_eq!(count(), 1);
+///
+/// // you can include arbitrary logic in this update function
+/// // also notifies subscribers, even though the value hasn't changed
+/// set_count.update(|n| if *n > 3 { *n += 1 });
+/// assert_eq!(count(), 1);
+/// # }).dispose();
+/// ```
+impl SignalSet for WriteSignal {
#[cfg_attr(
debug_assertions,
instrument(
@@ -633,9 +997,31 @@ where
)
)
)]
- pub fn set(&self, new_value: T) {
+ fn set(&self, new_value: T) {
self.id.update(self.runtime, |n| *n = new_value);
}
+
+ #[cfg_attr(
+ debug_assertions,
+ instrument(
+ level = "trace",
+ name = "WriteSignal::try_set()",
+ skip_all,
+ fields(
+ id = ?self.id,
+ defined_at = %self.defined_at,
+ ty = %std::any::type_name::()
+ )
+ )
+ )]
+ fn try_set(&self, new_value: T) -> Option {
+ let mut new_value = Some(new_value);
+
+ self.id
+ .update(self.runtime, |t| *t = new_value.take().unwrap());
+
+ new_value
+ }
}
impl Clone for WriteSignal {
@@ -652,38 +1038,6 @@ impl Clone for WriteSignal {
impl Copy for WriteSignal {}
-#[cfg(not(feature = "stable"))]
-impl FnOnce<(T,)> for WriteSignal
-where
- T: 'static,
-{
- type Output = ();
-
- extern "rust-call" fn call_once(self, args: (T,)) -> Self::Output {
- self.update(move |n| *n = args.0)
- }
-}
-
-#[cfg(not(feature = "stable"))]
-impl FnMut<(T,)> for WriteSignal
-where
- T: 'static,
-{
- extern "rust-call" fn call_mut(&mut self, args: (T,)) -> Self::Output {
- self.update(move |n| *n = args.0)
- }
-}
-
-#[cfg(not(feature = "stable"))]
-impl Fn<(T,)> for WriteSignal
-where
- T: 'static,
-{
- extern "rust-call" fn call(&self, args: (T,)) -> Self::Output {
- self.update(move |n| *n = args.0)
- }
-}
-
/// Creates a reactive signal with the getter and setter unified in one value.
/// You may prefer this style, or it may be easier to pass around in a context
/// or as a function argument.
@@ -724,6 +1078,28 @@ pub fn create_rw_signal(cx: Scope, value: T) -> RwSignal {
/// 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
/// its style, or it may be easier to pass around in a context or as a function argument.
+///
+/// ## Core Trait Implementations
+/// - [`.get()`](#impl-SignalGet-for-RwSignal) clones the current
+/// value of the signal. If you call it within an effect, it will cause that effect
+/// to subscribe to the signal, and to re-run whenever the value of the signal changes.
+/// - [`.get_untracked()`](#impl-SignalGetUntracked-for-RwSignal) clones the value of the signal
+/// without reactively tracking it.
+/// - [`.with()`](#impl-SignalWith-for-RwSignal) allows you to reactively access the signal’s value without
+/// cloning by applying a callback function.
+/// - [`.with_untracked()`](#impl-SignalWithUntracked-for-RwSignal) allows you to access the signal’s
+/// value without reactively tracking it.
+/// - [`.set()`](#impl-SignalSet-for-RwSignal) sets the signal’s value,
+/// and notifies all subscribers that the signal’s value has changed.
+/// to subscribe to the signal, and to re-run whenever the value of the signal changes.
+/// - [`.set_untracked()`](#impl-SignalSetUntracked-for-RwSignal) sets the signal’s value
+/// without notifying its subscribers.
+/// - [`.update()`](#impl-SignalUpdate-for-RwSignal) mutates the signal’s value in place
+/// and notifies all subscribers that the signal’s value has changed.
+/// - [`.update_untracked()`](#impl-SignalUpdateUntracked-for-RwSignal) mutates the signal’s value
+/// in place without notifying its subscribers.
+/// - [`.to_stream()`](#impl-SignalStream-for-RwSignal) converts the signal to an `async` stream of values.
+///
/// ```
/// # use leptos_reactive::*;
/// # create_scope(create_runtime(), |cx| {
@@ -768,7 +1144,7 @@ impl Clone for RwSignal {
impl Copy for RwSignal {}
-impl UntrackedGettableSignal for RwSignal {
+impl SignalGetUntracked